CompletableFuture error handling

Multithreading · lesson 27 of 38 · 4 min read

Recover, observe or transform a failure inside an async chain, and get timeouts under control.

Open this lesson in the learning hub

Key points

  • A failure skips every thenApply and thenCompose below it and travels down the chain untouched.
  • exceptionally replaces the failure with a fallback value, and the chain carries on as if nothing happened.
  • handle receives both the value and the exception, so it can fold either outcome into one result.
  • whenComplete only observes. The failure keeps travelling, which is exactly what you want for logging.
  • Crossing a stage boundary wraps the cause in CompletionException, so call getCause() before reading the message.
  • orTimeout fails the future with TimeoutException, and completeOnTimeout supplies a default instead.

Example

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) {
        // a failure skips every mapping stage until something handles it
        String recovered = CompletableFuture.<String>supplyAsync(() -> { throw new IllegalStateException("db down"); })
                .thenApply(s -> s + " (never runs)")
                .exceptionally(ex -> "fallback: " + ex.getCause().getMessage())
                .join();
        System.out.println("exceptionally : " + recovered);

        // handle sees both outcomes and may change the type
        String handled = CompletableFuture.supplyAsync(() -> 21 * 2)
                .handle((value, ex) -> ex == null ? "ok " + value : "failed " + ex.getMessage())
                .join();
        System.out.println("handle        : " + handled);

        // whenComplete observes only: the failure still travels on
        CompletableFuture<String> observed = CompletableFuture
                .<String>failedFuture(new IllegalArgumentException("bad id"))
                .whenComplete((v, ex) -> System.out.println("whenComplete  : saw " + ex.getClass().getSimpleName()));
        System.out.println("still failed  : " + observed.isCompletedExceptionally());

        // a timeout is just another failure you can recover from
        String timed = CompletableFuture.supplyAsync(() -> { sleep(500); return "slow"; })
                .orTimeout(100, TimeUnit.MILLISECONDS)
                .exceptionally(ex -> "recovered from " + ex.getClass().getSimpleName())
                .join();
        System.out.println("orTimeout     : " + timed);
    }

    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

Handle failure inside the chain, not after join() has already thrown.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Multithreading course, and every lesson in it is listed on the Multithreading contents page.