Что произойдёт, если в цепочке CompletableFuture возникнет исключение?
Исключение в цепочке CompletableFuture прерывает выполнение и передаётся downstream — все последующие стадии (thenApply, thenAccept, thenRun) не выполняются.
🟢 Junior Level
Исключение в цепочке CompletableFuture прерывает выполнение и передаётся downstream — все последующие стадии (thenApply, thenAccept, thenRun) не выполняются.
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> {
throw new RuntimeException("Error!");
})
.thenApply(s -> s.toUpperCase()); // Не выполнится
// future.isCompletedExceptionally() = true
// future.join() — бросает CompletionException
Простая аналогия:
- Цепочка CF — как конвейер
- Если на одном этапе ошибка — весь конвейер останавливается
- Нужно поставить “аварийный выход” (exceptionally/handle)
Обработка исключений
// 1. exceptionally — fallback значение
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> { throw new RuntimeException("Error!"); })
.exceptionally(ex -> "Fallback value");
// 2. handle — обработка и результата, и исключения
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> { throw new RuntimeException("Error!"); })
.handle((result, ex) -> ex != null ? "Fallback" : result);
// 3. whenComplete — наблюдение (не перехватывает!)
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> "Hello")
.whenComplete((result, ex) -> {
if (ex != null) {
log.error("Error", ex);
}
});
// Исключение всё ещё в CF — нужно exceptionally/handle
🟡 Middle Level
CompletionException
join() и get() заворачивают оригинальное исключение в CompletionException.
Оригинальное исключение доступно через getCause().
exceptionally получает CompletionException с оригинальным исключением как cause.
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> { throw new RuntimeException("Original error"); });
try {
future.join(); // бросает CompletionException
} catch (CompletionException e) {
Throwable cause = e.getCause(); // RuntimeException: "Original error"
}
Распространение исключения
supplyAsync(throws) → thenApply(пропускает) → thenAccept(пропускает) → exceptionally(перехватывает)
Исключение проходит через всю цепочку до первого обработчика
exceptionally vs handle vs whenComplete
| Метод | Перехватывает | Возвращает значение | Выполняется при успехе |
|---|---|---|---|
exceptionally |
Да | Да (fallback) | Нет |
handle |
Да | Да | Да |
whenComplete |
Нет (только наблюдение) | Нет (пробрасывает) | Да |
// whenComplete НЕ перехватывает — исключение остаётся в CF
cf.whenComplete((r, ex) -> log("done"))
.join(); // всё ещё бросает!
// handle перехватывает — CF завершается нормально
cf.handle((r, ex) -> ex != null ? "fallback" : r)
.join(); // OK
🔴 Senior Level
Internal Implementation
Исключение сохраняется в поле result как AltResult (внутренний класс), который оборачивает throwable. При вызове join()/get() он разворачивается в CompletionException.
// Упрощённо:
private volatile Object result; // может быть AltResult с Throwable
// completeExceptionally(Throwable ex)
// — устанавливает result = new AltResult(ex)
// — уведомляет все зависимые CF
Exception composition
// Несколько CF с обработкой ошибок
CompletableFuture<String> cf1 = service1()
.exceptionally(ex -> "fallback1");
CompletableFuture<String> cf2 = service2()
.exceptionally(ex -> "fallback2");
// allOf завершится с ошибкой только если оба упали
// и не обработали ошибки
CompletableFuture.allOf(cf1, cf2).join();
Best Practices
// ✅ Всегда добавляйте exceptionally/handle
cf.exceptionally(ex -> { log.error("...", ex); return defaultValue; });
// ✅ Проверяйте cause в CompletionException
catch (CompletionException e) { handleCause(e.getCause()); }
// ✅ whenComplete только для side-effects (логирование, метрики)
cf.whenComplete((r, ex) -> metrics.record(ex != null ? "error" : "ok"));
// ❌ Не игнорируйте исключения в whenComplete
// ❌ Не забывайте обработчик в конце цепочки
// ❌ Не оборачивайте исключения без сохранения cause
🎯 Шпаргалка для интервью
Обязательно знать:
- Исключение прерывает цепочку — все downstream thenApply/thenAccept/thenRun не выполняются
- join()/get() заворачивают в CompletionException, оригинал через getCause()
- exceptionally перехватывает и возвращает fallback, handle — полный контроль, whenComplete — только наблюдает
- Исключение сохраняется как AltResult (внутренний класс), не блокирует CF
- Обработчик в конце цепочки — обязателен для production
Частые уточняющие вопросы:
- whenComplete перехватит исключение? — Нет, только наблюдает. CF останется в состоянии ошибки
- exceptionally получает оригинальное исключение? — Нет, CompletionException. getCause() для оригинала
- Что будет с allOf если один CF упадёт? — allOf завершится с ошибкой. Нужен handle на каждом CF
- Исключение в exceptionally — что будет? — Новое исключение, ловится downstream exceptionally
Красные флаги (НЕ говорить):
- «whenComplete ловит исключения» — только наблюдает, ошибка остаётся в CF
- «exceptionally получает оригинальное исключение» — получает CompletionException
- «Исключение блокирует CF навсегда» — CF завершается exceptionally, не блокирует
Связанные темы:
- [[6. Как обрабатывать исключения в цепочке CompletableFuture]]
- [[7. В чём разница между handle(), exceptionally() и whenComplete()]]
- [[27. Как реализовать retry логику с помощью CompletableFuture]]
- [[20. Как реализовать timeout для CompletableFuture]]