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

Що станеться, якщо в ланцюжку CompletableFuture виникне виключення?

Виключення в ланцюжку CompletableFuture перериває виконання і передається downstream — усі наступні стадії (thenApply, thenAccept, thenRun) не виконуються.

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

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