CompletableFuture not working as expected
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
When CompletableFuture behaves unexpectedly, the cause is usually not randomness in the API. It is usually one of three things: the wrong stage method, hidden exceptions, or blocking in the wrong place. CompletableFuture becomes predictable once you are explicit about execution, composition, and error handling.
Start with Stage Semantics
One of the most common mistakes is using thenApply when the mapping function already returns another future.
The rule is:
- use
thenApplyfor value-to-value transformations - use
thenComposefor value-to-future transformations
If you use the wrong one, you often end up with nested futures or callbacks that look like they never finish.
Use Explicit Executors for Real Work
The default common pool is fine for simple demos, but it becomes confusing when tasks block or when you need predictable scheduling.
Using your own executor makes thread usage easier to reason about and debug.
Handle Exceptions Inside the Pipeline
A CompletableFuture that fails does not always fail where you expect. Many bugs come from exceptions surfacing later as CompletionException when join() is called.
If you expect failure as part of normal control flow, handle it in the chain rather than letting it explode at the outer boundary.
Avoid Blocking Inside Worker Stages
Another common problem is calling join() too early or too deep in the pipeline. That can cause starvation or deadlock-like behavior when combined with limited executors.
A better aggregation pattern is:
Block only at the outer boundary if you can avoid blocking elsewhere.
Timeouts and Cancellation Need a Policy
Without timeouts, a future chain can sit forever.
Timeout behavior should be intentional, not an afterthought.
Common Pitfalls
- Using
thenApplywherethenComposeis required and accidentally creating nested future logic. - Relying on the common pool for blocking work and then wondering why callbacks stall.
- Ignoring exceptional completion until
join()throws far away from the real cause. - Blocking inside worker stages instead of composing futures and blocking only at the edge.
- Forgetting to define timeout or cancellation behavior for operations that may hang.
Summary
- '
CompletableFutureproblems usually come from composition, execution policy, or error handling.' - Use
thenApplyfor values andthenComposefor inner futures. - Prefer explicit executors when the workload is real or partially blocking.
- Handle errors and timeouts inside the pipeline instead of treating them as surprises.
- Keep blocking at the outer boundary and let the async chain stay async internally.

