У чому різниця між handle(), exceptionally() і whenComplete()
Три методи для обробки помилок у CompletableFuture:
🟢 Junior Level
Три методи для обробки помилок у CompletableFuture:
exceptionally()— повернути fallback при помилці (якcatch)handle()— обробити і успіх, і помилку (якtry/catchз return)whenComplete()— виконати дію при завершенні, не змінюючи результат (якfinally)
// 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 — тільки при помилці:
fetchDataAsync()
.exceptionally(ex -> {
log.error("Fetch failed", ex);
return "default"; // fallback значення
});
handle — завжди викликається:
fetchDataAsync()
.handle((result, ex) -> {
if (ex != null) {
log.error("Failed", ex);
return "error";
}
return result.toUpperCase(); // можна змінити результат
});
whenComplete — side effect, не змінює результат:
fetchDataAsync()
.whenComplete((result, ex) -> {
// Не змінює результат — тільки side effect
if (ex == null) {
metrics.increment("success");
} else {
metrics.increment("failure");
}
})
.thenApply(s -> s.toUpperCase()); // результат не змінений
Порівняльна таблиця
| Метод | Можна змінити результат? | Можна змінити тип? | Поглинає помилку? | Коли викликається? |
|---|---|---|---|---|
| exceptionally | ✅ Так | ❌ Ні | ✅ Так | Тільки при помилці |
| whenComplete | ❌ Ні | ❌ Ні | ❌ Ні | Завжди |
| handle | ✅ Так | ✅ Так | ✅ Так | Завжди |
whenComplete: важлива поведінка
Якщо action сам викине виключення — НОВЕ виключення замінить оригінальне як результат стадії. Оригінальне буде втрачено (не додається як suppressed).
cf.whenComplete((result, ex) -> {
throw new RuntimeException("New error"); // замінить оригінальний ex!
});
// Оригінальне виключення втрачено!
Типові помилки
1. Null Handling в handle:
cf.handle((result, ex) -> {
// При успіху: ex == null, result != null
// При помилці: ex != null, result == null
// Перевірка на null — обов'язкова!
if (ex != null) return fallback;
return result.toUpperCase();
});
2. exceptionally не викликається при успіху:
cf.exceptionally(ex -> "fallback")
.thenAccept(s -> System.out.println(s));
// Якщо cf успішний — exceptionally пропущений, результат йде далі
🔴 Senior Level
exceptionallyCompose (Java 12+)
Це “flatMap” для обробки помилок. Звичайний exceptionally змушує вас повернути готовий об’єкт, а exceptionallyCompose дозволяє повернути інший CompletableFuture.
// "Якщо основний мікросервіс впав, піди в резервний асинхронно"
cf.exceptionallyCompose(ex -> {
if (isRetryable(ex)) {
return retryOperation(); // CompletableFuture<T>
}
return CompletableFuture.failedFuture(ex);
});
Thread Context
Усі три методи виконуються в тому ж потоці, який завершив попередній етап (якщо не використовувати *Async версії). Це може призвести до блокування “чужих” пулів.
// Небезпечно: виконується в потоці, що завершив CF
cf.whenComplete((r, ex) -> heavyOperation());
// Безпечно: перемикає потік
cf.whenCompleteAsync((r, ex) -> heavyOperation(), executor);
Коли НЕ використовувати handle
- Коли потрібна тільки обробка помилок — використовуйте
exceptionally(чіткіше за наміром) - Коли потрібен побічний ефект без зміни результату —
whenComplete
// ❌ handle коли достатньо exceptionally
cf.handle((r, ex) -> ex != null ? defaultValue : r); // надмірно
// ✅ exceptionally — чіткіше за наміром
cf.exceptionally(ex -> defaultValue);
// ❌ handle коли потрібен тільки side effect
cf.handle((r, ex) -> { log.info("done"); return r; }); // надмірно
// ✅ whenComplete — не змінює результат
cf.whenComplete((r, ex) -> log.info("done"));
Short-circuit
Якщо посередині довгого ланцюжка стоїть exceptionally, який повернув результат, усі наступні thenApply працюватимуть з цим результатом, наче помилки і не було.
cf.thenApply(s -> { throw new RuntimeException("Error"); })
.exceptionally(ex -> "fallback") // повернув "fallback"
.thenApply(s -> s.toUpperCase()); // працює з "fallback"!
Best Practices
// ✅ exceptionally для fallback
cf.exceptionally(ex -> defaultValue);
// ✅ handle для повного контролю
cf.handle((result, ex) -> ex != null ? fallback : transform(result));
// ✅ whenComplete для моніторингу
cf.whenComplete((r, ex) -> metrics.record(r, ex));
// ✅ exceptionallyCompose для ретраїв (Java 12+)
cf.exceptionallyCompose(ex -> isRetryable(ex) ? retry() : failedFuture(ex));
// ❌ Ігнорування помилок
// ❌ Блокуючі операції в обробниках
// ❌ handle коли достатньо exceptionally
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- exceptionally — тільки при помилці, повертає fallback, тип не змінюється
- handle — завжди викликається, може змінити тип, поглинає помилку
- whenComplete — завжди викликається, НЕ змінює результат, НЕ поглинає помилку
- handle = повний контроль, exceptionally = чистий fallback, whenComplete = моніторинг
- exceptionallyCompose (Java 12+) — async retry, повертає інший CF
Часті уточнюючі питання:
- whenComplete викине — що буде? — Нове виключення замінить оригінальне, оригінальне втрачено
- exceptionally може змінити тип? — Ні, повертає той же T. handle може змінити тип
- Коли exceptionallyCompose? — Коли при помилці потрібно запустити іншу async операцію (retry, fallback сервіс)
- handle vs exceptionally — коли що? — handle коли потрібна і обробка результату теж, exceptionally тільки для помилок
Червоні прапорці (НЕ говорити):
- «whenComplete перехоплює помилки» — він тільки спостерігає, помилка залишається в CF
- «handle і exceptionally це одне й те саме» — handle викликається завжди, exceptionally тільки при помилці
- «exceptionally може повернути інший тип» — ні, тільки T -> T
Пов’язані теми:
- [[6. Як обробляти виключення в ланцюжку CompletableFuture]]
- [[18. Що станеться, якщо в ланцюжку CompletableFuture виникне виключення]]
- [[27. Як реалізувати retry логіку за допомогою CompletableFuture]]
- [[20. Як реалізувати timeout для CompletableFuture]]