skip to content

CompletableFuture

Composing asynchronous work into pipelines: creating stages, chaining and combining them, handling failures, and choosing which executor runs each callback. It is the standard async API in modern Java services, so expect to build a small pipeline out loud.

part ofJavaoverview, primer and where to startread it →
on this pageshow

explore

questions

page 1 of 2

What does thenCombine do on a CompletableFuture, and when would you use it?

level: juniorimportance: must knowfreq 70%

answer

  1. Two independent futures → one combined value
  2. BiFunction(a, b) -> result
  3. Both must be started first → runs in parallel
  4. Either fails → combined fails, fn skipped
  5. Combine=value, AcceptBoth=side effect, runAfterBoth=ignore results

basics

~10 s

thenCombine waits for two independent CompletableFutures to both finish, then merges their two results into one new value using a function you supply.

solid answer

~40 s

thenCombine joins two independent CompletableFutures. You call it on one future, pass the other future plus a BiFunction; when both complete, the BiFunction receives both results and you return a combined value, which becomes the result of the new future. The two stages run concurrently (you start them before combining), so the total time is roughly the slower of the two rather than their sum. Use it when you need data from two unrelated async calls together, e.g. fetch a user and their account balance in parallel, then build a profile object. If either source future fails, the combined future completes exceptionally and the BiFunction is skipped. Variants: thenAcceptBoth takes a BiConsumer (returns CompletableFuture<Void>, no result) and runAfterBoth takes a Runnable (ignores both results) when you only care that both finished.

go deeper

for a junior

Knows thenCombine waits for two futures and merges their results with a function, and can write a basic supplyAsync + thenCombine example.

for a middle

Explains the parallelism (both must be started first → latency is the max, not the sum), failure short-circuiting, and the thenAcceptBoth/runAfterBoth distinction.

for a senior

Discusses async vs non-async overloads and which thread the callback runs on, contrasts thenCombine with thenCompose, and handles exceptions via handle/exceptionally downstream.

for a principal

Reasons about pool sizing and thread-handoff costs of the Async variants at scale, and when thenCombine's pairwise composition should give way to allOf-based aggregation for many stages.

## Background: what a CompletableFuture is A `CompletableFuture<T>` is a Java object representing a value of type `T` that may not exist yet because it is being computed asynchronously (often on another thread). It is a 'promise' of a future result. You attach callbacks that run automatically when the value becomes available, instead of blocking and waiting. ## The problem thenCombine solves Suppose you have **two independent** asynchronous computations — neither depends on the other's result. For example, calling two different web services. You want to start both, let them run **at the same time**, and then do something with **both** results once they are both ready. `thenCombine` is the tool for exactly this. ## Signature and meaning ``` <U,V> CompletableFuture<V> thenCombine( CompletionStage<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fn) ``` Read it as: 'I am a future producing a `T`. Here is `other`, another future producing a `U`. When **both** of us have completed, call `fn(myResult, otherResult)` and the value it returns (a `V`) becomes the result of the **new** future I hand back.' - `other` is the second future. A `CompletionStage` is the interface `CompletableFuture` implements; just think 'another future'. - `fn` is a `BiFunction` — a function taking **two** arguments and returning one. (A `Function` takes one argument; a `BiFunction` takes two.) ## Why it runs in parallel The two futures must already be **started** before you combine them. Typically: ```java CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> slowA()); CompletableFuture<Integer> b = CompletableFuture.supplyAsync(() -> slowB()); CompletableFuture<Integer> sum = a.thenCombine(b, (x, y) -> x + y); ``` Because `a` and `b` were both submitted to a thread pool (via `supplyAsync`) before the `thenCombine` line, they execute **concurrently**. The combining function only fires after the later of the two finishes, so total latency ≈ max(timeA, timeB), not timeA + timeB. (Pitfall: if you accidentally chain `b` off `a`, they become sequential and you lose the parallelism.) ## What happens on failure If **either** `a` or `b` completes exceptionally (throws), the returned future also completes exceptionally with that exception, and `fn` is **never** called. You handle that downstream with `exceptionally`, `handle`, or `whenComplete`. ## The sibling 'both' methods The same 'wait for both' idea comes in three flavours that differ only in what they do with the two results: | Method | Takes | Produces | Use when | |---|---|---|---| | `thenCombine` | `BiFunction<T,U,V>` | `CompletableFuture<V>` | you need a **combined value** | | `thenAcceptBoth` | `BiConsumer<T,U>` | `CompletableFuture<Void>` | you consume both for a **side effect**, no value | | `runAfterBoth` | `Runnable` | `CompletableFuture<Void>` | you only care that **both finished**, ignore results | (Each also has `...Async` overloads that run the callback on a supplied or common pool thread rather than on whichever thread completed the last input.) ## Deriving your answer - Junior: 'it waits for two futures and merges their results with a function.' - Senior: add the parallelism guarantee, the failure semantics, and the AcceptBoth/runAfterBoth distinction.

  • How is thenCombine different from thenCompose?
    thenCombine joins TWO already-running futures with a BiFunction and merges their results. thenCompose takes ONE future and a function that itself returns a future, flattening the nesting (a flatMap) for sequential dependent steps — the second call depends on the first's result.
  • If you only need to know that both futures finished but don't care about their values, what do you use?
    runAfterBoth, which takes a Runnable and returns CompletableFuture<Void>.

saying these in an interview costs you the question

  • Thinking thenCombine starts the second future — both must already be running for parallelism
  • Confusing thenCombine (two futures, BiFunction) with thenCompose (one future, flatMap of a function returning a future)
  • Claiming the combining function runs even if one input fails — it does not
  • Chaining the second future off the first, which makes them sequential not parallel

context

open as a page

How do you manually drive a CompletableFuture to completion, and what is the difference between complete(value) and completeExceptionally(throwable)?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A CompletableFuture isn't always finished automatically — you can finish it yourself. complete(value) makes it succeed with that value; completeExceptionally(ex) makes it fail with that exception. Both return true only if your call was the one that completed it.

open as a page

What are the main ways to create a CompletableFuture that runs a task asynchronously, and how do supplyAsync and runAsync differ?

level: juniorimportance: must knowfreq 70%

basics

~10 s

Use CompletableFuture.supplyAsync when the task returns a value, and runAsync when it returns nothing. supplyAsync takes a Supplier and gives a CompletableFuture<T>; runAsync takes a Runnable and gives a CompletableFuture<Void>.

open as a page

What does thenApply do on a CompletableFuture, and how does it differ from thenAccept and thenRun?

level: juniorimportance: must knowfreq 70%

basics

~10 s

thenApply transforms the result into a new value (map). thenAccept consumes the result but returns nothing. thenRun runs an action that ignores the result entirely. All three run after the stage completes.

open as a page

In CompletableFuture, what is the difference between thenApply and thenApplyAsync, and which thread runs the callback in each case?

level: middleimportance: must knowfreq 70%

basics

~10 s

thenApply may run the callback on whatever thread completed the previous stage (or the caller). thenApplyAsync hands the callback to a separate thread pool to run, so it does not block the completing thread.

open as a page

What is the difference between get() and join() on a CompletableFuture, and when would you choose each?

level: middleimportance: must knowfreq 75%

basics

~10 s

Both block until the result is ready. get() throws checked exceptions (InterruptedException, ExecutionException), so callers must handle them. join() throws unchecked exceptions (CompletionException), so it's cleaner inside lambdas and stream pipelines.

open as a page

On which thread pool do supplyAsync and runAsync run by default, and how do you override it?

level: middleimportance: must knowfreq 65%

basics

~20 s

By default they run on the shared ForkJoinPool.commonPool(). Each async method also has an overload that takes an Executor, so you pass your own thread pool as a second argument to control where the task runs.

open as a page

When an exception is thrown inside a CompletableFuture stage, how does it propagate down the chain? What is CompletionException and where does it come from?

level: middleimportance: must knowfreq 60%

basics

~10 s

If a stage throws, the future completes exceptionally and later stages are skipped until an error-handling callback. The original exception is usually wrapped in a CompletionException, so you often unwrap it via getCause().

open as a page

Compare exceptionally, handle, and whenComplete on CompletableFuture. When would you reach for each, and how do they differ in what they receive and what they produce?

level: middleimportance: must knowfreq 72%

basics

~20 s

exceptionally runs only on failure and supplies a fallback value. handle runs on both success and failure and returns a new value. whenComplete runs on both for a side-effect (like logging) but cannot change the result.

open as a page

Why is doing a blocking call (e.g. a synchronous HTTP request or JDBC query) inside a CompletableFuture callback that runs on the default executor dangerous, and how do you avoid it?

level: middleimportance: must knowfreq 70%

basics

~20 s

When you don't pass an executor, callbacks run on the shared ForkJoinPool.commonPool, which has only a few threads. Blocking those threads starves every other task using that pool. Fix it by passing your own dedicated executor to the *Async methods.

open as a page

How can a CompletableFuture silently swallow an exception, and what practices ensure failures are observed?

level: middleimportance: must knowfreq 65%

basics

~20 s

If a stage throws and you never call join()/get() and never add exceptionally()/handle(), the failure is stored inside the future and nobody ever sees it — no log, no crash. Always terminate a chain by either joining it or attaching an error handler.

open as a page

What is the difference between thenApply and thenCompose, and when must you use thenCompose?

level: middleimportance: must knowfreq 85%

basics

~10 s

Use thenApply when your function returns a plain value. Use thenCompose when your function itself returns another CompletableFuture, so you chain asynchronous steps without ending up with a CompletableFuture wrapped inside a CompletableFuture.

open as a page

Why is running blocking work on no-executor *Async stages (the ForkJoinPool common pool) risky, and what should you do instead?

level: seniorimportance: must knowfreq 62%

basics

~10 s

The no-executor *Async methods use one small shared ForkJoinPool for the whole JVM. Blocking it (I/O, waits) starves every other user of that pool. Pass your own executor sized for blocking work instead.

open as a page

Why does CompletableFuture.allOf return CompletableFuture<Void>, and how do you get the actual results out?

level: seniorimportance: must knowfreq 66%

basics

~10 s

allOf can take futures of different types, so it can't return one combined value — it returns Void, signalling only that all finished. You read each original future's result yourself afterward (e.g. with join).

open as a page

How do exceptions flow through a chain of thenApply/thenCompose stages, and how is that hidden by the returned types?

level: seniorimportance: must knowfreq 65%

basics

~10 s

If any stage throws or its upstream failed, that stage completes exceptionally and every following thenApply/thenCompose is skipped — the error 'short-circuits' to the end. You handle it with exceptionally, handle, or whenComplete.

open as a page

A teammate says 'using thenApply instead of thenApplyAsync means the callback runs synchronously on the thread that created the future.' What is wrong with this claim?

level: middleimportance: should knowfreq 48%

basics

~10 s

It's not the creating thread — it's the completing thread (whichever thread finishes the previous stage), or the caller if the stage was already done. The creating thread is usually irrelevant.

open as a page

Map the full grid of CompletableFuture two-input combinators by what they wait for and what callback they take.

level: middleimportance: should knowfreq 48%

basics

~20 s

There are six two-input methods in a 3x2 grid: wait for BOTH (thenCombine, thenAcceptBoth, runAfterBoth) or EITHER first (applyToEither, acceptEither, runAfterEither). Within each row the callback is a function (returns a value), a consumer (side effect), or a runnable (ignores results).

open as a page

What do applyToEither and acceptEither do, and how do they differ from the 'both' family?

level: middleimportance: should knowfreq 52%

basics

~10 s

The 'either' methods react to whichever of two futures finishes first. applyToEither passes that first result to a function; the 'both' methods instead wait for both futures to complete.

open as a page

How do you inspect a CompletableFuture's state without blocking, and what do getNow, isDone, isCancelled, and isCompletedExceptionally tell you?

level: middleimportance: should knowfreq 55%

basics

~20 s

These methods peek at the future without waiting. isDone tells you if it's finished (any way). isCancelled and isCompletedExceptionally tell you how it finished. getNow(default) returns the value right now if ready, otherwise your default — it never blocks.

open as a page

How do you create a CompletableFuture that you complete manually later, and when is that useful?

level: middleimportance: should knowfreq 50%

basics

~10 s

Create an empty future with new CompletableFuture<>(), then call complete(value) when the result arrives, or completeExceptionally(error) on failure. It's useful for bridging callback-based APIs into the CompletableFuture world.

open as a page

What are the common misconceptions about ordering and thread assignment when chaining multiple *Async stages on a CompletableFuture?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Dependent stages still run in order — each waits for the previous result. But *Async does not guarantee the same thread for each stage, and sibling stages attached to the same future can run concurrently in any order.

open as a page

What do orTimeout and completeOnTimeout do, and how do they differ from each other and from get(timeout)?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Both (Java 9+) put a time limit on the future itself, not just on a waiting caller. orTimeout fails it with a TimeoutException if it's late; completeOnTimeout finishes it with a fallback value instead. Unlike get(timeout), they actually settle the future.

open as a page

If the Supplier passed to CompletableFuture.supplyAsync throws an exception, what happens to the future and to a later get() call?

level: seniorimportance: should knowfreq 55%

basics

~10 s

The exception is captured and the future completes exceptionally—it does not propagate out of supplyAsync. A later get() then throws an ExecutionException whose cause is the thrown exception; join() throws a CompletionException wrapping it.

open as a page

Why might you choose CompletableFuture.supplyAsync over ExecutorService.submit, given both run a task on a thread pool and return a Future?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Both run a task and return a Future, but ExecutorService.submit gives a plain Future you can only block on with get(). supplyAsync returns a CompletableFuture you can chain non-blocking steps onto (thenApply, thenCompose, combine) and add error recovery.

open as a page

What is exceptionallyCompose and how does it differ from exceptionally? When is it the right tool?

level: seniorimportance: should knowfreq 38%

basics

~20 s

exceptionally returns a plain fallback value on error. exceptionallyCompose returns a whole new CompletableFuture on error — so the fallback can itself be asynchronous, like calling a backup service. It is the error-path version of thenCompose.

open as a page

When you block on a CompletableFuture that failed, how do join() and get() differ in the exception they throw? Why does that distinction exist?

level: seniorimportance: should knowfreq 55%

basics

~10 s

get() throws the checked ExecutionException (and you must catch it), while join() throws the unchecked CompletionException. Both wrap the real cause, so you call getCause() to reach it. join() is friendlier in lambdas.

open as a page

Explain the common misconception about thenApply vs thenApplyAsync ordering. Do the Async variants change the order in which dependent stages execute?

level: seniorimportance: should knowfreq 45%

basics

~20 s

No. thenApply and thenApplyAsync produce the same logical order — each stage still waits for the one before it. The 'Async' suffix only controls which thread the callback runs on, not whether stages run in parallel or reorder.

open as a page

Why does calling cancel() on a CompletableFuture often fail to actually stop the running work or propagate through a chain, and how should cancellation be designed instead?

level: seniorimportance: should knowfreq 40%

basics

~20 s

CompletableFuture.cancel() just completes the future with a CancellationException — it does not interrupt the thread running your task, and downstream-only cancellation doesn't travel back to upstream stages. To really stop work you need cooperative cancellation (check a flag / interrupt) and you must cancel the right (upstream) stages.

open as a page

What do the *Async variants (thenApplyAsync, etc.) change, and why does supplying your own Executor matter?

level: seniorimportance: should knowfreq 60%

basics

~20 s

The Async variants run the callback on a separate thread pool instead of whatever thread completed the previous stage. Without an Executor they use the shared common ForkJoinPool; passing your own pool keeps blocking work off that shared pool.

open as a page

How would you design the executor strategy for a CompletableFuture-based service that mixes CPU-bound transforms and blocking I/O across many stages?

level: principalimportance: should knowfreq 30%

basics

~20 s

Separate the work: keep cheap CPU transforms on non-async or the common pool, and give blocking I/O its own dedicated, bounded executor (or virtual threads). Use one pool per downstream so a slow dependency can't starve the rest.

open as a page

showing 1–30 of 35