В чём разница между 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]]