skip to content

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