Back to Blog
Java

Java CompletableFuture allOf: Wait for Multiple Futures

java completablefuture allof: Learn how to use CompletableFuture.allOf to wait for multiple asynchronous tasks, collect results, and handle failures in Java.

CompletableFutureConcurrencyAsynchronous ProgrammingJavaFuture
Illustration of Java CompletableFuture.allOf combining multiple asynchronous futures into a single completion point.

When you need to wait for several independent asynchronous tasks in Java, CompletableFuture.allOf is the standard way to combine them. This article explains how java completablefuture allof works, what it returns, and how to use it correctly in real code.

What CompletableFuture.allOf Actually Does

CompletableFuture.allOf takes a varargs array of CompletableFuture<?> and returns a new CompletableFuture<Void>. The returned future completes only when all input futures complete, whether normally or exceptionally. It does not wait for them sequentially; it registers a completion callback on each input and counts down internally.

The key point is that the result type is Void. You do not get the individual results directly from the combined future. Instead, you must query each original future after the combined one completes. This design keeps the API simple and avoids forcing a common type across heterogeneous futures.

Basic Usage: Waiting for Multiple Futures

The most straightforward use is to wait for several futures that run in parallel. For example, suppose you fetch data from three remote services concurrently:

CompletableFuture<String> userFuture = fetchUser(); CompletableFuture<String> orderFuture = fetchOrder(); CompletableFuture<String> invoiceFuture = fetchInvoice(); CompletableFuture<Void> all = CompletableFuture.allOf(userFuture, orderFuture, invoiceFuture); all.join(); // blocks until all three complete

After all.join() returns, you can safely call userFuture.get(), orderFuture.get(), and invoiceFuture.get() without blocking, because they are guaranteed to be done. This pattern is useful when you need to wait for a batch of independent tasks before proceeding.

Collecting Results from allOf

Because allOf returns Void, you need a way to gather the results. A common pattern is to chain a thenApply or thenRun on the combined future and collect the values inside that stage. Here is an example that combines the results into a list:

List<CompletableFuture<String>> futures = List.of(fetchUser(), fetchOrder(), fetchInvoice()); CompletableFuture<List<String>> combined = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList())); List<String> results = combined.join();

Notice that join() inside the thenApply is safe because the allOf stage has already completed, meaning all input futures are done. This avoids the nested blocking that would occur if you called join before the combined future finished.

Handling Exceptions and Failures

allOf completes normally even if some input futures complete exceptionally. It does not propagate the first exception. Instead, the combined future completes normally, and the exception remains stored in the individual future. This behavior surprises many developers who expect allOf to fail fast.

To handle failures, you must check each future individually after the combined future completes. For example:

CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3); all.join(); for (CompletableFuture<?> future : List.of(f1, f2, f3)) { try { future.get(); // throws ExecutionException if that future failed } catch (ExecutionException e) { // handle the failure for this specific future } }

If you need to fail fast when any future fails, use anyOf with a different strategy, or wrap each future with exceptionally to convert failures into default values before passing them to allOf. The latter is often cleaner when a partial failure is acceptable.

Combining allOf with thenApply and Other Stages

allOf is rarely the end of a pipeline. You typically chain further transformations on the combined future. Because the combined future is itself a CompletableFuture<Void>, you can use thenApply, thenAccept, or thenCompose to continue after all inputs are done.

For example, to run a final action after all downloads finish:

CompletableFuture<Void> all = CompletableFuture.allOf(download1, download2); CompletableFuture<String> summary = all.thenApply(v -> { // All downloads are complete; combine their contents return download1.join() + download2.join(); });

You can also use thenCompose to start another asynchronous task after the batch completes, avoiding blocking a thread.

Concurrency and Thread Pool Considerations

allOf itself does not create new threads. It relies on the thread pools used by the input futures. When you call join() on the combined future, the calling thread blocks until all inputs finish. If you call join() from a common pool thread, you risk thread starvation in high-concurrency scenarios.

A better approach is to use asynchronous chaining (thenApply, thenCompose) rather than blocking join() in the main flow. This allows the calling thread to return to the pool while the batch completes. For example:

CompletableFuture<List<String>> result = CompletableFuture.allOf(futures) .thenApply(v -> collectResults(futures)); result.thenAccept(results -> process(results));

If you must block, be aware that join() throws CompletionException if any input future completes exceptionally. This is a runtime exception, unlike get() which throws checked exceptions. Choose the method that matches your error-handling style.

When Not to Use allOf

allOf is not the right tool when you need the result of the first completed future, or when you need to fail fast on the first error. In those cases, anyOf or whenComplete on individual futures may be more appropriate.

Also, if you have a large number of futures (thousands), allOf creates a completion counter and registers callbacks on each future. The overhead is small but not zero. For extremely large batches, consider processing them in smaller groups or using a custom aggregator.

Finally, remember that allOf does not guarantee that the input futures start concurrently. It only waits for them. If you want to start tasks in parallel, create the futures with an executor that supports parallelism, such as a fixed thread pool. The default common pool may not give you the concurrency you expect in constrained environments.

Practical Example: Parallel API Calls with Timeouts

A realistic use case is calling multiple external APIs in parallel and combining their responses. Here is a complete example with a timeout applied to each call:

ExecutorService executor = Executors.newFixedThreadPool(3); CompletableFuture<String> user = CompletableFuture.supplyAsync(() -> api.getUser(), executor) .orTimeout(2, TimeUnit.SECONDS); CompletableFuture<String> orders = CompletableFuture.supplyAsync(() -> api.getOrders(), executor) .orTimeout(2, TimeUnit.SECONDS); CompletableFuture<String> invoice = CompletableFuture.supplyAsync(() -> api.getInvoice(), executor) .orTimeout(2, TimeUnit.SECONDS); CompletableFuture<List<String>> all = CompletableFuture.allOf(user, orders, invoice) .thenApply(v -> List.of(user.join(), orders.join(), invoice.join())); try { List<String> results = all.get(5, TimeUnit.SECONDS); // process results } catch (TimeoutException e) { // handle overall timeout } catch (ExecutionException e) { // handle one of the futures failing } executor.shutdown();

The orTimeout method ensures each individual future completes exceptionally if it takes too long. allOf still waits for all futures, but the combined get with a timeout provides an overall deadline. This pattern is common in service orchestration where you want to bound the total wait time.

java completablefuture allof: Practical Usage and Code Examp | RYUSLOG DEV