When a step throws

Read the explanation
What if

An exception skips the next steps until something handles it. exceptionally runs only on failure; handle always runs.

01234567start bothprice.join()printlnpriceAt(A) throwsp * 2io-1: exceptionallypriceAt(B)handle

Tap any block to see what that thread is doing.

  • Running
  • Waiting / parked
  • Error
  • Skipped
  • Waiting on I/O
Step 1/5

Two chains start. Shop A is down; shop B works.

1var price = CompletableFuture.supplyAsync(() -> priceAt("A"), io) // throws: shop is down2    .thenApply(p -> p * 2)        // skipped: there is no value3    .exceptionally(ex -> -1);     // runs only on failure: fallback -14 5var label = CompletableFuture.supplyAsync(() -> priceAt("B"), io) // works6    .handle((p, ex) -> ex == null ? "€" + p : "n/a"); // always runs7 8System.out.println(price.join() + " " + label.join()); // -1 €12

Explanation

What just happened

Shop A threw an exception, so the future held an error instead of a value. thenApply(p -> p * 2) was skipped: there was no p to double. exceptionally caught the error and replaced it with -1, and from there the chain carried on normally.

Shop B worked, and handle ran with (12, null).

Try the what-if switch

nobody handles it: the same failure with no join(), get() or handler at the end. The future quietly holds the exception, thenAccept is skipped, and nothing is printed: no stack trace, no log line.

The three tools

runs on success runs on failure can change the result
exceptionally(ex -> ...) no yes yes
handle((v, ex) -> ...) yes yes yes
whenComplete((v, ex) -> ...) yes yes no (only looks, e.g. for logging)

Think of it like try/catch

An exception jumps over every following step until it reaches one that handles it, just like code after a throw is skipped until the catch.

Watch out

Common questions

What is the difference between exceptionally and handle?

exceptionally runs only when the stage failed, and can replace the error with a value. handle always runs, with both the value and the exception (one of them is null), and can change the result.

What happens to the steps after an exception?

They are skipped. The exception passes over every following step until one handles it, like code after a throw until the catch.

Why does my CompletableFuture exception disappear?

Nothing read the result. The future quietly holds the exception: no stack trace, no log line. End every chain with join(), exceptionally, handle or whenComplete.

Verified: this timeline matches a real program, checked in CI on JDK 21 and 25. How we verify. Unclear or wrong? Tell us