Command Palette

Search for a command to run...

PHASE 13Advanced Java 8+ ~18 min· topic 7 of 11

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).

Main.javawhole filejava
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).

Running independent calls in paralleldiagram
Rendering diagram…

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. 1

    thenApply instead of thenCompose

    In the first example, change thenCompose to thenApply. Read the compile error: the result type becomes CompletableFuture<CompletableFuture<Integer>>, which can't be added to an int.

  2. 2

    Fail a different step

    In "Recovering from failure", make the supplier return 5 and throw inside thenApply instead. The fallback still runs: errors from any earlier stage reach exceptionally.

Code & diagrams

A pipeline with thenApply, thenCompose and thenCombine Java 8+ New tab
Sign in to run this example in your browser.

Expected output

ASHA pays 70
Recovering from failure Java 8+ New tab
Sign in to run this example in your browser.

Expected output

fallback because: inventory service down
stock shown: 0
status: failed
allOf: wait for many, read in order Java 9+ New tab
Sign in to run this example in your browser.

Expected output

Pune: 16 C
Delhi: 20 C
Chennai: 28 C
Timeouts (Java 9) Java 9+ New tab

completeOnTimeout doesn't stop the slow task; it just stops waiting for it. Cancel work separately if it's expensive.

Sign in to run this example in your browser.

Expected output

cached answer

Break 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.

terminal
$ observe the service
── what you'll see ──
Unrelated parallel streams slow to a crawl; thread dumps show every ForkJoinPool.commonPool worker blocked in socket reads.

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 CompletableFuture holds a result field and a lock-free stack of dependent completions (Completion objects) pushed with CAS; completing it pops and fires them, which is why callbacks can run on the completing thread.

  • ▸

    cancel(true) on a CompletableFuture completes it with CancellationException but does not interrupt the running supplier, unlike FutureTask. Propagate cancellation yourself if the work is long.

  • ▸

    In Java 19+, Future.state(), resultNow() and exceptionNow() let you inspect a finished future without try/catch; CompletableFuture.exceptionallyCompose (Java 12) recovers with another async step.

Remember this

  1. 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>** implements Future<T> and CompletionStage<T>, letting you attach callbacks that run when the value arrives.

  2. 2

    Start work with supplyAsync(supplier) (returns a value) or runAsync(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. 3

    Transform with **thenApply(fn)** (like map), consume with thenAccept, run an action with thenRun. Chain a step that itself returns a future with **thenCompose(fn)** (like flatMap), so you get a flat CompletableFuture<User> instead of a nested one. Combine two independent futures with **thenCombine(other, (a, b) -> ...), and many with allOf(...)** (wait for all) or anyOf (first to finish).

  4. 4

    Failures travel down the chain: if a stage throws, later thenApply stages are skipped and the future completes exceptionally. Recover with **exceptionally(ex -> fallback), or handle both outcomes with handle((value, ex) -> ...)**. Exceptions arrive wrapped in CompletionException; join() rethrows them unchecked, get() as ExecutionException.

  5. 5

    Methods without Async run the callback on whichever thread completes the previous stage (or the caller, if it's already done). The ...Async variants (thenApplyAsync) always hand the callback to an executor. Java 9 added timeouts: **orTimeout(duration) fails the future and completeOnTimeout(value, duration)** completes it with a default.

  6. 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. CompletableFuture remains useful for combining results and for APIs that already return futures.

Explain it without notes

01

What can CompletableFuture do that a plain Future can't?

02

Explain the difference between thenApply and thenCompose.

03

How do exceptions propagate through a CompletableFuture chain, and how do you handle them?

Practice

01

Start two async tasks that return 6 and 7, multiply their results with thenCombine and print the product.

02

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.