Що станеться, якщо в ланцюжку 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]]