What happens if an exception occurs in CompletableFuture chain
An exception in a CompletableFuture chain interrupts execution and propagates downstream — all subsequent stages (thenApply, thenAccept, thenRun) are not executed.
🟢 Junior Level
An exception in a CompletableFuture chain interrupts execution and propagates downstream — all subsequent stages (thenApply, thenAccept, thenRun) are not executed.
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> {
throw new RuntimeException("Error!");
})
.thenApply(s -> s.toUpperCase()); // Will not execute
// future.isCompletedExceptionally() = true
// future.join() — throws CompletionException
Simple analogy:
- CF chain is like an assembly line
- If one stage errors out — the whole line stops
- You need an “emergency exit” (exceptionally/handle)
Exception handling
// 1. exceptionally — fallback value
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> { throw new RuntimeException("Error!"); })
.exceptionally(ex -> "Fallback value");
// 2. handle — handles both result and exception
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> { throw new RuntimeException("Error!"); })
.handle((result, ex) -> ex != null ? "Fallback" : result);
// 3. whenComplete — observation only (does not catch!)
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> "Hello")
.whenComplete((result, ex) -> {
if (ex != null) {
log.error("Error", ex);
}
});
// Exception is still in CF — need exceptionally/handle
🟡 Middle Level
CompletionException
join() and get() wrap the original exception in a CompletionException.
The original exception is accessible via getCause().
exceptionally receives a CompletionException with the original exception as cause.
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> { throw new RuntimeException("Original error"); });
try {
future.join(); // throws CompletionException
} catch (CompletionException e) {
Throwable cause = e.getCause(); // RuntimeException: "Original error"
}
Exception propagation
supplyAsync(throws) → thenApply(skips) → thenAccept(skips) → exceptionally(catches)
The exception travels through the entire chain until the first handler
exceptionally vs handle vs whenComplete
| Method | Catches | Returns value | Executes on success |
|---|---|---|---|
exceptionally |
Yes | Yes (fallback) | No |
handle |
Yes | Yes | Yes |
whenComplete |
No (observation only) | No (passes through) | Yes |
// whenComplete does NOT catch — exception stays in CF
cf.whenComplete((r, ex) -> log("done"))
.join(); // still throws!
// handle catches — CF completes normally
cf.handle((r, ex) -> ex != null ? "fallback" : r)
.join(); // OK
🔴 Senior Level
Internal Implementation
The exception is stored in the result field as an AltResult (internal class) which wraps the throwable. When join()/get() is called, it is unwrapped into a CompletionException.
// Simplified:
private volatile Object result; // can be AltResult with Throwable
// completeExceptionally(Throwable ex)
// — sets result = new AltResult(ex)
// — notifies all dependent CFs
Exception composition
// Multiple CFs with error handling
CompletableFuture<String> cf1 = service1()
.exceptionally(ex -> "fallback1");
CompletableFuture<String> cf2 = service2()
.exceptionally(ex -> "fallback2");
// allOf will only fail if both failed
// and didn't handle errors
CompletableFuture.allOf(cf1, cf2).join();
Best Practices
// ✅ Always add exceptionally/handle
cf.exceptionally(ex -> { log.error("...", ex); return defaultValue; });
// ✅ Check cause in CompletionException
catch (CompletionException e) { handleCause(e.getCause()); }
// ✅ whenComplete only for side-effects (logging, metrics)
cf.whenComplete((r, ex) -> metrics.record(ex != null ? "error" : "ok"));
// ❌ Don't ignore exceptions in whenComplete
// ❌ Don't forget a handler at the end of the chain
// ❌ Don't wrap exceptions without preserving cause
🎯 Interview Cheat Sheet
Must know:
- Exception breaks the chain — all downstream thenApply/thenAccept/thenRun are not executed
- join()/get() wrap in CompletionException, original via getCause()
- exceptionally catches and returns fallback, handle — full control, whenComplete — only observes
- Exception is stored as AltResult (internal class), does not block CF
- Handler at the end of the chain — mandatory for production
Common follow-up questions:
- Does whenComplete catch exceptions? — No, only observes. CF remains in error state
- Does exceptionally receive the original exception? — No, CompletionException. Use getCause() for the original
- What happens to allOf if one CF fails? — allOf completes exceptionally. Need handle on each CF
- Exception in exceptionally — what happens? — New exception, caught by downstream exceptionally
Red flags (DO NOT say):
- “whenComplete catches exceptions” — only observes, error remains in CF
- “exceptionally receives the original exception” — receives CompletionException
- “Exception blocks CF forever” — CF completes exceptionally, does not block
Related topics:
- [[6. How to handle exceptions in CompletableFuture chain]]
- [[7. What is the difference between handle(), exceptionally() and whenComplete()]]
- [[27. How to implement retry logic with CompletableFuture]]
- [[21. How to implement timeout for CompletableFuture]]