Which thread runs each step?

Read the explanation
What if

supplyAsync → thenApply → thenAccept. Each step starts when the one before it finishes, and gets its result.

01234567main: build chainjoin()continuesfetchUser(42)worker-1: user.name()println

Tap any block to see what that thread is doing.

  • Running
  • Waiting / parked
  • Waiting on I/O
Step 1/5

supplyAsync starts fetchUser on a common-pool thread. The other steps are only attached.

1CompletableFuture2    .supplyAsync(() -> fetchUser(42))              // starts on a pool thread3    .thenApply(user -> user.name())                // after fetchUser4    .thenAccept(name -> System.out.println(name))  // after thenApply5    .join();                                       // main waits for the chain

Explanation

What just happened

main described a three-step pipeline and then waited on join(). A common-pool thread ran the steps in order: fetchUser, then thenApply with the user, then thenAccept with the name. Each step started the moment the previous one finished, with its result.

No thread waits between steps. main only waits at the very end, because this program has nothing else to do.

Try the what-if switch

thenApply vs thenApplyAsync: the same parse callback runs on three different threads.

  1. main, for done.thenApply(...): done was already complete when the callback was attached.
  2. io-1, for f.thenApply(...): io-1 is the thread that completed f.
  3. cpu-1, for f.thenApplyAsync(..., cpuPool): async means “submit to this executor”.

The rule

Watch out

You don’t control where a plain thenApply runs. Put heavy work there and it might run on an HTTP client’s I/O thread. Use thenApplyAsync with an executor you own for slow or blocking work.

supplyAsync without an executor uses the shared common pool, whose threads are daemons. If main exits first, the pipeline may never finish.

Common questions

Which thread runs thenApply?

The thread that completes the future. If the future is already complete when you call thenApply, the calling thread runs the callback at once.

Which thread runs thenApplyAsync?

A thread of the executor you pass. Without one, it uses ForkJoinPool.commonPool(). It doesn't inherit the executor of the stage before it.

Why does my CompletableFuture print nothing?

supplyAsync without an executor runs on the common pool, whose threads are daemon threads. If main ends first, the JVM can exit before the pipeline finishes. Call join() or pass your own executor.

Verified: this timeline matches a real program, checked in CI on JDK 21 and 25. How we verify. Unclear or wrong? Tell us