On this page
CompletableFuture
CompletableFuture (since Java 8) supports non-blocking asynchronous programming with composable pipelines for chaining, combining, and handling async results.
Creating CompletableFuture
// Run async task with no return value
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("Running async");
});
// Supply a result asynchronously
CompletableFuture<String> supply = CompletableFuture.supplyAsync(() -> {
return fetchDataFromApi();
});
// Already completed
CompletableFuture<String> completed = CompletableFuture.completedFuture("done");
By default, runAsync and supplyAsync use the ForkJoinPool.commonPool(). Pass a custom executor for production:
ExecutorService executor = Executors.newFixedThreadPool(4);
CompletableFuture.supplyAsync(() -> fetchData(), executor);
Chaining Operations
CompletableFuture.supplyAsync(() -> " hello ")
.thenApply(String::trim)
.thenApply(String::toUpperCase)
.thenAccept(result -> System.out.println(result)) // HELLO
.join(); // wait for completion
| Method | Input | Output |
|---|---|---|
thenApply(fn) |
T | U |
thenAccept(consumer) |
T | Void |
thenRun(runnable) |
— | Void |
thenCompose(fn) |
T | CompletableFuture<U> |
thenCombine(other, fn) |
T, U | V |
thenApply vs thenCompose
// thenApply — flatten one level
CompletableFuture<Integer> length = CompletableFuture
.supplyAsync(() -> "hello")
.thenApply(String::length); // CompletableFuture<Integer>
// thenCompose — flatten nested futures
CompletableFuture<User> user = CompletableFuture
.supplyAsync(() -> "[email protected]")
.thenCompose(email -> findUserByEmail(email)); // returns CompletableFuture<User>
Combining Futures
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");
// Wait for both
CompletableFuture<String> combined = future1.thenCombine(future2, (a, b) -> a + " " + b);
// Wait for all
CompletableFuture<Void> all = CompletableFuture.allOf(future1, future2);
all.join();
// Wait for any
CompletableFuture<Object> any = CompletableFuture.anyOf(future1, future2);
Exception Handling
CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("Failed");
return "Success";
})
.exceptionally(ex -> {
System.out.println("Error: " + ex.getMessage());
return "Fallback";
})
.handle((result, ex) -> {
if (ex != null) return "Recovered";
return result;
});
Getting Results
// Blocking
String result = future.get(); // waits indefinitely
String result2 = future.get(5, TimeUnit.SECONDS); // with timeout
// Non-blocking check
if (future.isDone()) {
String r = future.getNow("default");
}
// join() — like get() but wraps in unchecked CompletionException
String r = future.join();
Real-World Example
public CompletableFuture<OrderSummary> getOrderSummary(Long orderId) {
CompletableFuture<Order> orderFuture =
CompletableFuture.supplyAsync(() -> orderService.findById(orderId));
CompletableFuture<List<Item>> itemsFuture =
CompletableFuture.supplyAsync(() -> itemService.findByOrderId(orderId));
CompletableFuture<User> userFuture = orderFuture
.thenCompose(order -> userService.findByIdAsync(order.getUserId()));
return orderFuture
.thenCombine(itemsFuture, (order, items) -> order)
.thenCombine(userFuture, (order, user) ->
new OrderSummary(order, itemsFuture.join(), user));
}
Best Practices
- Always use a dedicated
ExecutorServicein production — do not rely on the common pool - Handle exceptions with
exceptionallyorhandle - Avoid blocking calls (
get()) inside async pipelines - Use
thenCompose(notthenApply) when the next step returns aCompletableFuture - Shut down custom executors when the application stops