CompletableFuture
AdvancedCompletableFuture enables non-blocking async pipelines — chain transformations, combine results, and handle errors without blocking threads.
Overview
CompletableFuture (Java 8) is a powerful async framework built on Future. Unlike Future.get() which always blocks, CompletableFuture supports callback-based chaining: thenApply (transform result), thenCompose (chain dependent futures), thenCombine (combine two independent futures), allOf (wait for all), anyOf (first to complete). It also supports exception handling with exceptionally() and handle(). By default, callbacks run on the ForkJoinPool.commonPool().
Async Execution and Basic Chaining
CompletableFuture.supplyAsync() runs a Supplier asynchronously. thenApply() transforms the result (like map). thenAccept() consumes it without returning. thenRun() runs an action with no access to the result.
All these methods return a new CompletableFuture, enabling fluent chaining.
import java.util.concurrent.CompletableFuture;
// Run async, transform result
CompletableFuture<String> cf = CompletableFuture
.supplyAsync(() -> fetchUserFromDb(42)) // async
.thenApply(user -> user.getEmail()) // transform
.thenApply(String::toUpperCase); // transform again
// Non-blocking callback
cf.thenAccept(email ->
System.out.println("Email: " + email));
// Block only at the end if you need the value
String email = cf.join(); // like get() but throws unchecked
// Run with specific executor (avoid hogging common pool)
ExecutorService ioPool = Executors.newFixedThreadPool(10);
CompletableFuture<String> withPool = CompletableFuture
.supplyAsync(() -> callExternalApi(), ioPool)
.thenApplyAsync(response -> parse(response), ioPool);Combining Futures
thenCompose() chains dependent futures (flatMap — second depends on first result). thenCombine() combines two independent futures when both complete.
allOf() waits for all futures to complete; anyOf() completes when the first one does.
// thenCompose — sequential dependent async calls (flatMap)
CompletableFuture<Order> orderFuture = CompletableFuture
.supplyAsync(() -> getUserId("alice"))
.thenCompose(userId -> // userId needed for next call
CompletableFuture.supplyAsync(() -> getLatestOrder(userId)));
// thenCombine — two independent calls, combine results
CompletableFuture<String> userFuture =
CompletableFuture.supplyAsync(() -> fetchUser(1));
CompletableFuture<String> orderFuture2 =
CompletableFuture.supplyAsync(() -> fetchOrder(1));
CompletableFuture<String> combined = userFuture.thenCombine(
orderFuture2,
(user, order) -> user + " placed " + order);
// allOf — wait for all
CompletableFuture<Void> all = CompletableFuture.allOf(
userFuture, orderFuture2);
all.thenRun(() -> System.out.println("Both done"));
// anyOf — first to complete wins
CompletableFuture<Object> first = CompletableFuture.anyOf(
CompletableFuture.supplyAsync(() -> fetchFromCache()),
CompletableFuture.supplyAsync(() -> fetchFromDb()));Error Handling
exceptionally() catches an exception and provides a fallback value (like catch). handle() is called regardless of success or failure — it receives the result and the exception (one will be null).
whenComplete() is like handle() but cannot transform the result — used for side effects like logging.
CompletableFuture<String> cf = CompletableFuture
.supplyAsync(() -> callExternalApi()) // may throw
.thenApply(response -> parse(response))
// Catch exception, return fallback
.exceptionally(ex -> {
log.error("API failed: " + ex.getMessage());
return "fallback-value";
});
// handle() — always called, can transform both success and error
CompletableFuture<String> robust = CompletableFuture
.supplyAsync(() -> callApi())
.handle((result, ex) -> {
if (ex != null) {
return "error: " + ex.getCause().getMessage();
}
return result.toUpperCase();
});
// Timeout (Java 9+)
CompletableFuture<String> withTimeout = CompletableFuture
.supplyAsync(() -> slowOperation())
.orTimeout(3, TimeUnit.SECONDS) // completes exceptionally if slow
.exceptionally(ex -> "timed out");Interactive Visualization
Key Points to Remember
- supplyAsync() runs async; thenApply() transforms (map); thenCompose() chains (flatMap).
- thenCombine() joins two independent futures; allOf() waits for all; anyOf() takes the first.
- exceptionally() handles errors with a fallback; handle() processes both success and failure.
- join() is like get() but throws unchecked — prefer it in lambda chains.
- Always specify a custom executor for I/O tasks to avoid starving the common ForkJoinPool.
Practice CompletableFuture in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaWhat is the difference between thenApply() and thenCompose()?
What is the difference between allOf() and anyOf()?
How does exceptionally() differ from handle()?
Why should you avoid using the common ForkJoinPool for I/O-heavy CompletableFuture tasks?
How would you implement a timeout for a CompletableFuture pipeline?
Ask Aria about CompletableFuture
Your personal AI tutor — ask anything about this concept