How do you handle exceptions in a CompletableFuture pipel… — Cracked Java
// Concurrency & Multithreading · Callable, Future & CompletableFuture
SeniorTheoryCoding

How do you handle exceptions in a CompletableFuture pipeline (exceptionally/handle/whenComplete)?

A failure anywhere in the chain short-circuits the remaining then* stages and propagates as a CompletionException. You intercept it with exceptionally (recover only on error), handle (transform result or error into a new result), or whenComplete (observe both without changing them).

How failure propagates

If any stage throws, the future completes exceptionally. Every downstream thenApply/thenCompose is skipped, and the exception flows down until a handler catches it. Note it is wrapped: the original cause becomes the getCause() of a CompletionException.

exceptionally — recover on error only

Runs only if the stage failed. It maps the throwable to a fallback value of the same type; on success it is transparent.

CompletableFuture<Price> price =
    fetchPrice()
        .exceptionally(ex -> {            // only fires on failure
            log.warn("price lookup failed", ex);
            return Price.DEFAULT;          // recovery value
        });

Java 12 added exceptionallyCompose for an async fallback (returning a future) and exceptionallyAsync to run recovery on a pool.

handle — always runs, sees both

handle is invoked whether the stage succeeded or failed; it receives (result, throwable) where exactly one is non-null. It can swallow, rethrow, or transform.

CompletableFuture<String> result =
    callService()
        .handle((value, ex) -> {
            if (ex != null) return "fallback:" + ex.getMessage();
            return "ok:" + value;
        });

Use handle when you want a single place to fold success and failure into one value.

whenComplete — observe, don't transform

whenComplete also sees (result, throwable) but its return is ignored — it cannot change the outcome. It is for side effects (logging, metrics, cleanup). If whenComplete itself throws, that new exception replaces the original.

callService()
    .whenComplete((value, ex) -> {
        if (ex != null) metrics.increment("service.error");
        timer.stop();                      // result is unchanged
    });
Need to recover only on error, same type?   -> exceptionally
Need one value from success OR failure?      -> handle
Just logging/metrics, keep the outcome?      -> whenComplete
Choosing a handler

Mark your status