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
- The
exyou receive is usually aCompletionExceptionwrapping the real error. Checkex.getCause(). - End every chain with something that reads the result or the error:
join(),exceptionally,handleorwhenComplete.
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