Як обробляти виключення в ланцюжку 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]]