skip to content

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

level: seniorimportance: must knowfreq 66%

answer

  1. allOf → CompletableFuture<Void> (heterogeneous inputs, no aggregate type)
  2. Void just signals 'all done'; null value
  3. Keep original futures; join() each after — no real blocking
  4. thenApply(v -> futures.stream().map(join).collect())
  5. Failure: allOf fails but other inputs still run; guard each for partial results

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

solid answer

~50 s

allOf(cf1, cf2, ...) returns a CompletableFuture<Void> that completes when every input completes (or completes exceptionally if any input fails). It returns Void because the inputs may have heterogeneous types, so there is no single sensible aggregate value. To get results you keep references to the original futures and, after the allOf completes, call join() (or get()) on each — those calls won't block because the futures are already done. A common idiom is allOf over a List<CompletableFuture<T>>, then thenApply that maps the list with future.join() into a List<T>, optionally collected with a Stream. Two gotchas: if any input fails, allOf completes exceptionally but the *other* inputs still run; and calling join inside the thenApply is safe only because allOf guarantees completion first. For homogeneous lists this pattern gives you a clean 'wait for all, then collect' aggregator.

go deeper

for a junior

Knows allOf waits for all futures and that it doesn't directly return their results.

for a middle

Explains why allOf returns Void (heterogeneous input types) and that results are read from the original futures afterward.

for a senior

Writes the allOf + thenApply(join-collect) idiom correctly, explains why join doesn't block there, and handles the 'others still run on failure' and partial-results cases.

for a principal

Reasons about executor sizing and saturation for large N, structured concurrency / cancellation of in-flight stages on failure, and backpressure or batching when fanning out thousands of stages.

## The aggregation problem With `thenCombine` you can join **two** futures. But what about **many** — a dynamic list of N futures, e.g. one HTTP call per item in a list? Pairwise combining gets unwieldy. `CompletableFuture.allOf` is the fan-in (gather) primitive for an arbitrary number of stages. ## Signature ``` static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs) ``` It takes a **varargs array** of futures of **any** result types (`<?>` = unknown/heterogeneous) and returns a `CompletableFuture<Void>`. ## Why Void? Because the inputs can be of **different types** — `CompletableFuture<String>`, `CompletableFuture<Integer>`, `CompletableFuture<Order>` all mixed. There is **no single type** that could meaningfully represent 'all their results combined', so the API makes no attempt: it returns `Void`, whose only job is to **signal completion** ('all done'). The `Void` carries no data; `null` is its value. ## When does it complete? The returned future completes when **every** input has completed. If **any** input completes **exceptionally**, the `allOf` future also completes exceptionally (with one of the failures). Importantly, a failure does **not** stop the other inputs — they all still run; `allOf` just reflects the aggregate status. ## How to actually get the results You must keep references to the **original** futures. After (or as a continuation of) the `allOf`, you read each one. Because `allOf` guarantees they are all done, calling `join()` (which blocks until done and rethrows failures unchecked) returns **immediately**: ```java List<CompletableFuture<String>> futures = urls.stream() .map(url -> CompletableFuture.supplyAsync(() -> fetch(url))) .collect(Collectors.toList()); CompletableFuture<List<String>> all = CompletableFuture .allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> futures.stream() .map(CompletableFuture::join) // safe: all are already complete .collect(Collectors.toList())); ``` Here `thenApply(v -> ...)` receives the `Void` (`v` is `null`, ignored) and maps each original future to its value via `join`. The `join` calls do **not** block meaningfully because the gate (`allOf`) already ensured completion. ## `join` vs `get` - `get()` throws **checked** `InterruptedException`/`ExecutionException` (clutters lambdas). - `join()` throws **unchecked** `CompletionException` — friendlier inside streams. Both block until done; here completion is already guaranteed so neither truly blocks. ## Failure handling nuance If you call `join` on a future that failed, it rethrows. So if any input failed, the `thenApply`'s first failing `join` propagates and your `all` future fails. If you instead want **partial results** (collect successes, tolerate failures), guard **each** future with `.exceptionally(ex -> fallback)` **before** the `allOf`, so none of them is in a failed state when you join. ## allOf vs anyOf The sibling `anyOf(cfs...)` returns `CompletableFuture<Object>` that completes with the **first** result (any type, hence `Object`). `allOf` = wait for **all** (Void); `anyOf` = first **of many** (Object). They are the many-input analogues of `thenCombine` (both) and `applyToEither` (either). ## Deriving your answer - Middle: 'allOf returns Void because the inputs may differ in type; read each original future after.' - Senior: add the `join`-in-`thenApply` idiom, the 'others still run on failure' caveat, and the partial-results guard. - Principal: discuss pool sizing for N concurrent stages and ordering/backpressure when N is large.

  • Why is it safe to call join() inside the thenApply after allOf?
    allOf only completes once every input future has completed, so by the time thenApply runs, each original future is already done. join() therefore returns immediately rather than blocking.
  • How do you collect partial results when some futures fail?
    Attach .exceptionally(ex -> fallbackValue) (or handle) to each individual future BEFORE passing them to allOf. Then none is in a failed state, allOf completes normally, and join on each yields either the real result or the fallback.
  • What is the difference between allOf and anyOf?
    allOf waits for ALL inputs and returns CompletableFuture<Void> (no aggregate value). anyOf completes with the FIRST input to finish and returns CompletableFuture<Object> (any type, so it's Object).

saying these in an interview costs you the question

  • Expecting allOf to return the collected results directly — it returns Void
  • Thinking a failure cancels the remaining inputs — they all still run
  • Calling get/join on the inputs BEFORE allOf completes, which blocks serially and defeats parallelism
  • Forgetting to keep references to the original futures (you can't recover results from the Void)
  • Assuming allOf gives partial results on failure without per-future exceptionally guards

context