Как обрабатывать исключения в цепочке CompletableFuture
В CompletableFuture есть 3 основных способа обработки исключений:
🟢 Junior Level
В CompletableFuture есть 3 основных способа обработки исключений:
exceptionally()— вернуть значение по умолчанию при ошибкеhandle()— обработать и результат, и ошибкуwhenComplete()— выполнить действие при завершении (не меняет результат)
// 1. exceptionally — fallback значение
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Error");
}).exceptionally(ex -> "default value"); // вернёт "default value"
// 2. handle — обработка и успеха, и ошибки
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Error");
}).handle((result, ex) -> {
if (ex != null) return "error occurred";
return result;
});
// 3. whenComplete — просто узнать о завершении
CompletableFuture.supplyAsync(() -> "Hello")
.whenComplete((result, ex) -> {
if (ex != null) {
System.err.println("Error: " + ex);
} else {
System.out.println("Result: " + result);
}
});
🟡 Middle Level
Детальное сравнение
exceptionally — как try-catch:
CompletableFuture<String> cf = fetchDataAsync()
.exceptionally(ex -> {
log.error("Fetch failed", ex);
return "default"; // fallback значение
});
handle — полный контроль:
CompletableFuture<String> cf = fetchDataAsync()
.handle((result, ex) -> {
if (ex != null) {
log.error("Failed", ex);
return "error";
}
return result.toUpperCase(); // можно изменить результат
});
whenComplete — side effect:
CompletableFuture<String> cf = fetchDataAsync()
.whenComplete((result, ex) -> {
// Не меняет результат — только side effect
if (ex == null) {
metrics.increment("success");
} else {
metrics.increment("failure");
}
})
.thenApply(s -> s.toUpperCase()); // результат не изменён
Типичные ошибки
- exceptionally ЛОВИТ исключения из thenApply: ```java // ✅ Исключение в thenApply поймаётся в downstream exceptionally cf.thenApply(s -> { throw new RuntimeException(“Error”); }).exceptionally(ex -> “default”); // ✅ OK, ловит
// ✅ Но лучше handle для полного контроля cf.handle((result, ex) -> { if (ex != null) return “default”; return result; });
2. **CompletionException wrapping:**
```java
// Исключения оборачиваются в CompletionException
cf.exceptionally(ex -> {
// ex — CompletionException, не оригинальное исключение!
Throwable cause = ex.getCause(); // оригинальное
return "default";
});
🔴 Senior Level
Internal Implementation
exceptionally:
public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn) {
return uniExceptionallyStage(fn);
}
// Вызывается только при ошибке
// Если успех — fn не вызывается
// Возвращает fallback значение
handle:
public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn) {
return uniHandleStage(null, fn);
}
// Вызывается ВСЕГДА — и при успехе, и при ошибке
// result — значение (null при ошибке)
// ex — исключение (null при успехе)
whenComplete:
public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action) {
return uniWhenCompleteStage(null, action);
}
// Вызывается ВСЕГДА
// НЕ меняет результат — просто side effect
// Исключение из action оборачивается
Архитектурные Trade-offs
| Метод | Всегда вызывается | Меняет результат | Ловит ошибки |
|---|---|---|---|
| exceptionally | ❌ Только при ошибке | ✅ Fallback | ✅ |
| handle | ✅ | ✅ Полный контроль | ✅ |
| whenComplete | ✅ | ❌ Side effect only | ❌ |
Edge Cases
1. Multiple error handlers:
cf.exceptionally(ex -> {
log.error("First handler", ex);
return "first";
}).exceptionally(ex -> {
// Не вызывается — первый уже обработал
return "second";
});
2. Exception in handler:
cf.exceptionally(ex -> {
throw new RuntimeException("Handler error"); // Новое исключение
}).exceptionally(ex -> {
// Поймает исключение из первого handler
return "recovered";
});
3. handle для трансформации:
// handle может изменить тип
cf.<String>handle((result, ex) -> {
if (ex != null) {
return "Error: " + ex.getMessage();
}
return "Success: " + result;
});
Производительность
exceptionally: ~5 ns (только при ошибке)
handle: ~10 ns (всегда)
whenComplete: ~8 ns (всегда)
Overhead negligible compared to async operations
Production Experience
Retry logic:
public <T> CompletableFuture<T> withRetry(
Supplier<CompletableFuture<T>> operation,
int maxRetries
) {
return operation.get()
.exceptionallyCompose(ex -> {
if (maxRetries > 0 && isRetryable(ex)) {
log.warn("Retry after error", ex);
// В production добавьте задержку:
return CompletableFuture.delayedExecutor(100, MILLISECONDS)
.thenCompose(v -> withRetry(operation, maxRetries - 1));
}
return CompletableFuture.failedFuture(ex);
});
}
// Java 12+
cf.exceptionallyCompose(ex -> {
if (shouldRetry(ex)) {
return retryOperation();
}
return CompletableFuture.failedFuture(ex);
});
Circuit breaker pattern:
public CompletableFuture<Response> callWithCircuitBreaker(Request req) {
if (circuitBreaker.isOpen()) {
return CompletableFuture.failedFuture(
new CircuitBreakerOpenException());
}
return callAsync(req)
.handle((result, ex) -> {
if (ex != null) {
circuitBreaker.recordFailure();
throw new CompletionException(ex);
}
circuitBreaker.recordSuccess();
return result;
});
}
Best Practices
// ✅ exceptionally для fallback
cf.exceptionally(ex -> defaultValue);
// ✅ handle для полного контроля
cf.handle((result, ex) -> ex != null ? fallback : result);
// ✅ whenComplete для мониторинга
cf.whenComplete((r, ex) -> metrics.record(r, ex));
// ❌ Игнорирование ошибок
// ❌ Блокирующие операции в обработчиках
// ❌ Создание новых исключений без причины
🎯 Шпаргалка для интервью
Обязательно знать:
- exceptionally(Function<Throwable, T>) — fallback при ошибке, как catch
- handle(BiFunction<T, Throwable, U>) — всегда вызывается, полный контроль
- whenComplete(BiConsumer<T, Throwable>) — side effect only, не меняет результат
- Исключения оборачиваются в CompletionException, оригинал через getCause()
- exceptionallyCompose (Java 12+) — возврат другого CF при ошибке (для retry)
Частые уточняющие вопросы:
- whenComplete перехватывает исключения? — Нет, только наблюдает. Исключение остаётся в CF
- exceptionally вызывается при успехе? — Нет, только при ошибке
- Как сделать retry? — exceptionallyCompose (Java 12+) или рекурсия в exceptionally
- Можно ли обработать ошибку в нескольких handler? — Да, но первый перехвативший поглотит
Красные флаги (НЕ говорить):
- «whenComplete ловит исключения» — он только наблюдает, не перехватывает
- «exceptionally вызывается всегда» — только при ошибке, handle вызывается всегда
- «Исключение в handler не обработается» — handler исключения тоже ловится downstream exceptionally
Связанные темы:
- [[7. В чём разница между handle(), exceptionally() и whenComplete()]]
- [[18. Что произойдёт, если в цепочке CompletableFuture возникнет исключение]]
- [[27. Как реализовать retry логику с помощью CompletableFuture]]
- [[1. Что такое CompletableFuture и чем он отличается от Future]]