Java
CompletableFuture
multithreading
concurrency
programming best practices

What is the recommended way to wait till the Completable future threads finish

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

The recommended way to wait for multiple CompletableFuture tasks to finish is usually CompletableFuture.allOf(...), followed by reading the individual results. For a single future, join() is often the simplest choice unless you specifically need checked exceptions from get().

The more important design question is not only how to wait, but where to wait. In good asynchronous code, you often compose futures first and block only at the outer boundary of the program, such as in main, a test, or a framework integration point.

Waiting for One Future

For one future, the two common methods are:

  • 'join()'
  • 'get()'

Example:

java
1CompletableFuture<String> future =
2    CompletableFuture.supplyAsync(() -> "done");
3
4String result = future.join();
5System.out.println(result);

join() throws unchecked CompletionException, which often makes calling code simpler.

Equivalent with get():

java
String result = future.get();

That works too, but it forces you to handle checked exceptions such as InterruptedException and ExecutionException.

Waiting for Multiple Futures With allOf

If you have several futures and want to wait until all of them finish, allOf is the standard approach.

java
1import java.util.List;
2import java.util.concurrent.CompletableFuture;
3
4public class Main {
5    public static void main(String[] args) {
6        CompletableFuture<String> a =
7            CompletableFuture.supplyAsync(() -> "A");
8
9        CompletableFuture<String> b =
10            CompletableFuture.supplyAsync(() -> "B");
11
12        CompletableFuture<String> c =
13            CompletableFuture.supplyAsync(() -> "C");
14
15        CompletableFuture<Void> all =
16            CompletableFuture.allOf(a, b, c);
17
18        all.join();
19
20        List<String> results = List.of(a.join(), b.join(), c.join());
21        System.out.println(results);
22    }
23}

allOf returns a CompletableFuture<Void>, so you still need to read the individual results from the original futures afterward.

This pattern is recommended because:

  • it clearly expresses “wait for all”
  • it keeps the composition in the CompletableFuture API
  • it works well with pipelines before the final blocking point

It is generally better than manually looping and calling join() one future at a time when your intent is “all tasks must finish before I continue.”

Collecting Results Cleanly

If you already have a list of futures, you can combine waiting and result collection neatly:

java
1import java.util.List;
2import java.util.concurrent.CompletableFuture;
3import java.util.stream.Collectors;
4
5List<CompletableFuture<Integer>> futures = List.of(
6    CompletableFuture.supplyAsync(() -> 1),
7    CompletableFuture.supplyAsync(() -> 2),
8    CompletableFuture.supplyAsync(() -> 3)
9);
10
11CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
12
13List<Integer> values = futures.stream()
14    .map(CompletableFuture::join)
15    .collect(Collectors.toList());
16
17System.out.println(values);

This is a common production pattern because it scales cleanly to many futures.

When You Should Not Wait Immediately

A frequent mistake is blocking too early. CompletableFuture is most useful when you compose asynchronous work first:

java
1CompletableFuture<String> result =
2    fetchUser()
3        .thenCombine(fetchPermissions(), (user, permissions) ->
4            user + ":" + permissions
5        );

If you call join() too early on intermediate stages, you lose much of the value of asynchronous composition.

The usual recommendation is:

  • compose futures as long as possible
  • block only at the boundary where a synchronous answer is truly required

join() Versus get()

In practice:

  • use join() when you want simpler code and can accept unchecked exceptions
  • use get() when you are in an API that already expects checked exception handling

Neither is magically “more asynchronous.” Both block the caller when invoked.

What About Executor Shutdown

Sometimes developers ask how to “wait till CompletableFuture threads finish” when the real issue is executor management. If you created your own ExecutorService, you may also need to shut it down properly:

java
executor.shutdown();

But shutting down the executor is not the same thing as waiting for a particular set of futures. allOf(...).join() answers the future-completion question directly.

Handling Failures

If one future fails, allOf(...).join() completes exceptionally. That is usually what you want, but it means you should decide how to handle partial failure.

For example:

java
1try {
2    CompletableFuture.allOf(a, b, c).join();
3} catch (Exception ex) {
4    System.out.println("At least one task failed");
5}

If partial results are acceptable, you may need a different design with per-future exception handling.

Common Pitfalls

One common mistake is manually sleeping or polling instead of using the future API itself. That defeats the point of CompletableFuture.

Another mistake is calling join() on each future immediately after creating it, which serializes the workflow instead of letting tasks overlap.

It is also easy to think allOf returns all results directly. It does not. It only gives you a future that completes when all supplied futures complete.

Finally, if you use a custom executor, do not forget its lifecycle. Waiting for futures and managing the executor are related but separate responsibilities.

Summary

  • For one future, join() is usually the simplest waiting method.
  • For many futures, CompletableFuture.allOf(...).join() is the standard way to wait for all of them.
  • Read results from the original futures after allOf completes.
  • Prefer future composition first and blocking only at the outer boundary.
  • Distinguish waiting for futures from shutting down the executor that runs them.

Course illustration
Course illustration

All Rights Reserved.