Питання 7 · Розділ 19

У чому різниця між handle(), exceptionally() і whenComplete()

Три методи для обробки помилок у CompletableFuture:

Мовні версії: English Russian Ukrainian

🟢 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]]