CompletableFuture
The Future interface returned by ExecutorService.submit() tells you a result is coming, but it gives you almost no way to react to it without blocking. The only real option is to call .get(), which pauses the calling thread until the result is ready — you cannot chain a follow-up action, combine it with another future, or attach error-handling logic. CompletableFuture, added in Java 8, fixes exactly this gap: it is a Future you can compose, turning asynchronous code into readable pipelines instead of tangled callbacks or blocking waits.
Starting an async task
CompletableFuture.supplyAsync() runs a task on a background thread (by default, the common ForkJoinPool) and immediately gives you back a CompletableFuture representing its eventual result — no blocking required to obtain the handle itself.
supplyAsync — running work asynchronously
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Fetching on " + Thread.currentThread().getName());
return 42;
});
System.out.println("Main thread keeps going immediately...");
future.thenAccept(result -> System.out.println("Got result: " + result));
// In a real app you wouldn't normally join() on main —
// this just keeps the JVM alive long enough to see the output.
future.join();
}
}Chaining a pipeline
.thenApply() transforms the result once it arrives (like map), .thenAccept() consumes it without returning a new value (like forEach), and .thenCompose() chains onto another CompletableFuture-returning step (like flatMap) so you don't end up with a nested future of a future.
A small async pipeline
import java.util.concurrent.CompletableFuture;
public class Main {
static CompletableFuture<String> fetchUserName(int id) {
return CompletableFuture.supplyAsync(() -> "user-" + id);
}
static CompletableFuture<String> fetchGreeting(String userName) {
return CompletableFuture.supplyAsync(() -> "Hello, " + userName + "!");
}
public static void main(String[] args) {
CompletableFuture<String> pipeline = fetchUserName(7)
.thenApply(String::toUpperCase) // transform: "USER-7"
.thenCompose(Main::fetchGreeting) // chain to another async step
.thenApply(msg -> msg + " Welcome back.");
pipeline.thenAccept(System.out::println);
pipeline.join();
}
}Combining multiple futures
.thenCombine() waits for two independent futures and merges their results, while CompletableFuture.allOf() waits for an entire array of futures to finish (useful when you have many parallel calls, like fetching several resources at once).
Combining two independent async results
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture<Integer> price = CompletableFuture.supplyAsync(() -> 100);
CompletableFuture<Double> taxRate = CompletableFuture.supplyAsync(() -> 0.08);
CompletableFuture<Double> total = price.thenCombine(
taxRate,
(p, rate) -> p + (p * rate)
);
total.thenAccept(t -> System.out.println("Total: " + t));
total.join();
}
}Handling errors
.exceptionally() supplies a fallback value if any step in the chain throws, while .handle() receives both the result and the exception (whichever one is non-null) so you can react to success and failure in a single place.
Recovering from a failed async step
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (true) {
throw new RuntimeException("service unavailable");
}
return 99;
}).exceptionally(ex -> {
System.out.println("Recovered from: " + ex.getMessage());
return -1; // fallback value
});
System.out.println("Result: " + future.join()); // Result: -1
}
}CompletableFuture (Java 8+) makes asynchronous code composable, unlike plain Future which only supports blocking
.get().supplyAsync()starts a task on a background thread and immediately returns a handle to its future result..thenApply()transforms a result,.thenAccept()consumes it,.thenCompose()chains to another async step..thenCombine()merges two futures' results;allOf()waits for a whole collection of futures..exceptionally()and.handle()let you recover from or inspect a failure without ever blocking.