Вопрос 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]]