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:
join() throws unchecked CompletionException, which often makes calling code simpler.
Equivalent with 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.
allOf returns a CompletableFuture<Void>, so you still need to read the individual results from the original futures afterward.
Why allOf(...).join() Is Usually Recommended
This pattern is recommended because:
- it clearly expresses “wait for all”
- it keeps the composition in the
CompletableFutureAPI - 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:
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:
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:
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:
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
allOfcompletes. - Prefer future composition first and blocking only at the outer boundary.
- Distinguish waiting for futures from shutting down the executor that runs them.

