Topic 13.7
Future and CompletableFuture
In one line
CompletableFuture (Java 8) is a Future you can build pipelines on: start work asynchronously, transform results with thenApply, chain dependent calls with thenCompose, combine independent ones with thenCombine/allOf, and handle failures with exceptionally/handle, without blocking a thread at every step.
Think of it like this
Ordering food on a delivery app. You don't stand at the restaurant door waiting. You get a promise that food will arrive, and you set up what happens next: "when the food is ready, deliver it; when it's delivered, notify me; if anything goes wrong, refund me". CompletableFuture is that promise plus the chain of "when this, then that" steps.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Asynchronous
- Started now, finished later, while the caller carries on with other work.
- CompletableFuture
- A future you can complete yourself and attach next steps to, added in Java 8.
- Callback
- A function you hand over to be called later, when a result is ready.
- thenApply / thenCompose
- Transform a result (map), or continue with another asynchronous step (flatMap).
- Complete exceptionally
- Finish with an error instead of a value.
- Common pool
- The JVM-wide ForkJoinPool used by default for async tasks and parallel streams.
- join
- On a CompletableFuture: wait for the result and return it, throwing an unchecked exception if it failed.
Step by step
01From blocking Future to callbacks
With a plain Future, code reads like: submit, then get() and block, then use the value. With CompletableFuture you describe the whole pipeline up front and only block once at the end (or never, if a web framework consumes the future).
CompletableFuture<String> greeting = CompletableFuture
.supplyAsync(() -> loadUserName(42)) // runs on a pool thread
.thenApply(name -> "Hello, " + name) // transform the result
.exceptionally(ex -> "Hello, guest"); // recover from a failure
System.out.println(greeting.join());02thenApply vs thenCompose
If the next step returns a plain value, use thenApply. If it returns another CompletableFuture (another async call), use thenCompose; thenApply would give you a CompletableFuture<CompletableFuture<T>>, which nobody wants.
It's the same idea as map and flatMap on streams and Optional (Topics 10.5 and 10.7).
03Running independent calls in parallel
Fetching a user's profile and their orders don't depend on each other. Start both, then thenCombine them: the total time is the slower of the two, not the sum.
For a list of futures, CompletableFuture.allOf(array).join() waits for all; then read each with join() (no more waiting).
04How errors move through the chain
A throw in any stage completes that stage exceptionally; downstream thenApply/thenAccept are skipped; the first exceptionally or handle sees the exception (wrapped in CompletionException) and can produce a normal value again.
Put exceptionally where a fallback makes sense, not blindly at the end: catching everything at the end can hide which step failed.
05Which thread runs my callback?
thenApply runs on the thread that completed the previous stage, or immediately on the calling thread if the previous stage was already done. That's efficient but can surprise you (a slow callback blocks a pool thread). thenApplyAsync(fn, executor) gives you control.
Never run blocking I/O on the common pool: it's shared with parallel streams and sized for CPU work, so blocking it slows unrelated code. Pass a dedicated executor (or a virtual-thread executor) to supplyAsync.
Try it yourself
- 1
thenApply instead of thenCompose
In the first example, change
thenComposetothenApply. Read the compile error: the result type becomesCompletableFuture<CompletableFuture<Integer>>, which can't be added to an int. - 2
Fail a different step
In "Recovering from failure", make the supplier return 5 and throw inside
thenApplyinstead. The fallback still runs: errors from any earlier stage reachexceptionally.
Code & diagrams
Expected output
ASHA pays 70Expected output
fallback because: inventory service down
stock shown: 0
status: failedExpected output
Pune: 16 C
Delhi: 20 C
Chennai: 28 CcompleteOnTimeout doesn't stop the slow task; it just stops waiting for it. Cancel work separately if it's expensive.
Expected output
cached answerBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Blocking the common pool
Run many supplyAsync(() -> slowHttpCall()) without an executor while a parallel stream also runs.
Myth vs fact
Myth
thenApply always runs on another thread.
Fact
It runs on the completing thread, or on the caller if the stage is already complete. Use the Async variants to choose an executor.
Myth
exceptionally at the end catches only the last step's errors.
Fact
It sees a failure from any earlier stage, because exceptions skip ahead through the chain.
Myth
orTimeout cancels the running task.
Fact
It only completes the future exceptionally; the task keeps running unless it checks for cancellation.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Internally each
CompletableFutureholds a result field and a lock-free stack of dependent completions (Completionobjects) pushed with CAS; completing it pops and fires them, which is why callbacks can run on the completing thread. - ▸
cancel(true)on aCompletableFuturecompletes it withCancellationExceptionbut does not interrupt the running supplier, unlikeFutureTask. Propagate cancellation yourself if the work is long. - ▸
In Java 19+,
Future.state(),resultNow()andexceptionNow()let you inspect a finished future without try/catch;CompletableFuture.exceptionallyCompose(Java 12) recovers with another async step.
Remember this
- 1
A plain
Future(Topic 13.6) only lets you wait (get) or poll. To do something with the result you must block a thread until it's ready. **CompletableFuture<T>** implementsFuture<T>andCompletionStage<T>, letting you attach callbacks that run when the value arrives. - 2
Start work with
supplyAsync(supplier)(returns a value) orrunAsync(runnable). Without an executor argument they use the shared **ForkJoinPool.commonPool()**; pass your own executor for I/O work so you don't starve that shared pool.completedFuture(value)makes an already-finished one, handy in tests. - 3
Transform with **
thenApply(fn)** (likemap), consume withthenAccept, run an action withthenRun. Chain a step that itself returns a future with **thenCompose(fn)** (likeflatMap), so you get a flatCompletableFuture<User>instead of a nested one. Combine two independent futures with **thenCombine(other, (a, b) -> ...), and many withallOf(...)** (wait for all) oranyOf(first to finish). - 4
Failures travel down the chain: if a stage throws, later
thenApplystages are skipped and the future completes exceptionally. Recover with **exceptionally(ex -> fallback), or handle both outcomes withhandle((value, ex) -> ...)**. Exceptions arrive wrapped inCompletionException;join()rethrows them unchecked,get()asExecutionException. - 5
Methods without
Asyncrun the callback on whichever thread completes the previous stage (or the caller, if it's already done). The...Asyncvariants (thenApplyAsync) always hand the callback to an executor. Java 9 added timeouts: **orTimeout(duration)fails the future andcompleteOnTimeout(value, duration)** completes it with a default. - 6
With virtual threads (Java 21, Topic 13.10), simple blocking code in many cheap threads often replaces long callback chains, because blocking is cheap again.
CompletableFutureremains useful for combining results and for APIs that already return futures.
Explain it without notes
What can CompletableFuture do that a plain Future can't?
Explain the difference between thenApply and thenCompose.
How do exceptions propagate through a CompletableFuture chain, and how do you handle them?
Practice
Start two async tasks that return 6 and 7, multiply their results with thenCombine and print the product.
Make a chain that parses "x12" as an int asynchronously and falls back to -1 on failure.
Trade-offs
- ↔
Callback pipelines avoid blocking threads but are harder to read and debug than straight-line code; virtual threads make the straight-line style cheap again.
- ↔
The common pool is convenient but shared; dedicated executors isolate workloads at the cost of managing them.
Done when you can
Done when you can build a pipeline with supplyAsync, thenApply, thenCompose and thenCombine.
Done when you can wait for many futures with allOf and read results in order.
Done when you can handle failures with exceptionally and handle, and add timeouts.
Done when you know which thread runs callbacks and why I/O shouldn't use the common pool.