CodeGym /Courses /JAVA 25 SELF /Combining CompletableFuture: thenCombine, allOf, anyOf

Combining CompletableFuture: thenCombine, allOf, anyOf

JAVA 25 SELF
Level 55, Lesson 2
Available

1. Combining CompletableFuture

In real life, results rarely come from a single source: a user profile and their orders can be loaded in parallel, data from two microservices can be merged, and sometimes you just want to process the first response that arrives. A synchronous approach forces you to wait in sequence; the CompletableFuture class lets you launch everything at once and combine the results elegantly. It provides dedicated methods for this: thenCombine, allOf, anyOf. Let’s go through each in order.

Parallel tasks: two asynchronous requests

What this would look like synchronously:

String name = loadUserName();    // long-running
int balance = loadUserBalance(); // long-running
System.out.println("Name: " + name + ", Balance: " + balance);

The problem: the second call will start only after the first completes.

Asynchronous approach

With CompletableFuture, both tasks can be started at the same time:

CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> loadUserName());
CompletableFuture<Integer> balanceFuture = CompletableFuture.supplyAsync(() -> loadUserBalance());

But how do we now get both results and handle them together? That’s what thenCombine is for.

2. thenCombine: merging the results of two tasks

The thenCombine method lets you combine two CompletableFutures and execute an action when both tasks complete. It returns a new CompletableFuture with the combined result.

Signature:

<A, B, C> CompletableFuture<C> thenCombine(
    CompletionStage<? extends B> other,
    BiFunction<? super A, ? super B, ? extends C> fn
)
  • A — the type of the first future’s result,
  • B — the type of the second,
  • C — the type of the combined result.

Example

CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> loadUserName());
CompletableFuture<Integer> balanceFuture = CompletableFuture.supplyAsync(() -> loadUserBalance());

CompletableFuture<String> resultFuture = nameFuture.thenCombine(
    balanceFuture,
    (name, balance) -> "Name: " + name + ", Balance: " + balance
);

resultFuture.thenAccept(System.out::println);

How it works:

  • Both futures start in parallel.
  • As soon as both complete, the function (name, balance) -> ... is called.
  • The resulting future contains the string with the result.

Small example with numbers

CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> 3);
CompletableFuture<Integer> sum = f1.thenCombine(f2, Integer::sum);

sum.thenAccept(result -> System.out.println("Sum: " + result));

Output:

Sum: 5

Asynchronous variant

If the combine operation is heavy, use thenCombineAsync:

f1.thenCombineAsync(f2, (a, b) -> a * b);

3. allOf: when there are many tasks

What if we have not two tasks, but a whole dozen? For example, we want to load data for ten users in parallel. For this, there is the CompletableFuture.allOf method.

Description

CompletableFuture.allOf(f1, f2, ..., fn) returns a new future that completes when all the given tasks complete. But there’s a nuance: this future does not contain a result — its type is always CompletableFuture<Void>. To get the results, you need to separately pull them out of the original futures.

Example

CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "First");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "Second");

CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2);

all.thenRun(() -> {
    // All tasks are completed!
    String s1 = f1.join(); // join() — like get(), but throws an unchecked exception
    String s2 = f2.join();
    System.out.println(s1 + " & " + s2);
});

Output:

First & Second

Example with an array of tasks

List<CompletableFuture<String>> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
    int id = i;
    futures.add(CompletableFuture.supplyAsync(() -> "User " + id));
}

CompletableFuture<Void> all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));

all.thenRun(() -> {
    for (CompletableFuture<String> f : futures) {
        System.out.println(f.join());
    }
});

What happens:

  • All tasks start in parallel.
  • allOf completes when all tasks complete.
  • In the thenRun block, we can get the results via join().

Visual diagram

[Future1]   \
[Future2] ----> [allOf] ---> thenRun
[Future3]   /

4. anyOf: wait for the first task to complete

Sometimes you don’t need to wait for all tasks, but rather get the result of the fastest one. For example, we request data from two servers — we use the data from whichever replies first. That’s what CompletableFuture.anyOf is for.

Description

CompletableFuture.anyOf(f1, f2, ..., fn) returns a future that completes when any of the given tasks completes. The result type is CompletableFuture<Object>, because the task types may differ.

Example

CompletableFuture<String> fast = CompletableFuture.supplyAsync(() -> {
    sleep(500);
    return "Fast server";
});
CompletableFuture<String> slow = CompletableFuture.supplyAsync(() -> {
    sleep(2000);
    return "Slow server";
});

CompletableFuture<Object> any = CompletableFuture.anyOf(fast, slow);

any.thenAccept(result -> System.out.println("Received: " + result));

Output:

Received: Fast server

Example with different types
You can combine tasks of different types, but then the result will be of type Object, and an explicit cast will be required.

5. Useful nuances

Recommendations

  • allOf does not return an array of results. You must keep the original futures to get their values via join() or get().
  • anyOf returns the first completed result, but the type is Object. If all tasks have the same type, you can cast accordingly.
  • If one of the tasks in allOf completes exceptionally, the resulting future also completes exceptionally.
  • For thenCombine, both tasks must complete successfully; otherwise, an exception is thrown.

Method comparison table

Method When to use Result type
thenCombine
When you need to combine the results of two tasks Combined result
allOf
When you need to wait for all tasks to complete
Void
anyOf
When you need to wait for any one of the tasks to complete Object (the first task’s result)

6. Common mistakes when combining CompletableFuture

Error #1: Waiting for a result via get()/join() on the main thread.
If you write asynchronous code but still call get() or join() at the end, you block the thread and lose the benefits of asynchrony. Prefer thenAccept/thenRun to handle results without blocking.

Error #2: Not keeping references to the original futures when using allOf.
If you call CompletableFuture.allOf(f1, f2, f3) but don’t keep f1, f2, f3, you won’t be able to get their results. allOf returns only Void!

Error #3: Not handling errors in the chain.
If one of the tasks completes exceptionally, the entire allOf or thenCombine also completes exceptionally. Use error-handling methods (exceptionally, handle, whenComplete) so you don’t miss exceptions.

Error #4: Type mismatch with anyOf.
anyOf returns an Object. If your futures return different types, you’ll have to determine what arrived first. It’s better to use the same task types if possible.

Error #5: Chains that are too complex without comments.
As code grows, chains of futures can turn into “spaghetti”. Don’t hesitate to break a chain into separate variables and comment the steps.

1
Task
JAVA 25 SELF, level 55, lesson 2
Locked
Supply Coordination: Waiting for All Shipments
Supply Coordination: Waiting for All Shipments
1
Task
JAVA 25 SELF, level 55, lesson 2
Locked
Greeting Assembly: Combining Greeting Fragments
Greeting Assembly: Combining Greeting Fragments
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION