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