Combine Async Results with CompletableFuture in Java
Problem
A CompletableFuture represents a result that will be available later; it can be chained and combined with other futures without blocking a thread while waiting.
Fetch a product's price and a tax rate asynchronously, then compute the total price once both are available.
Java Program
import java.util.concurrent.CompletableFuture;
public class CompletableFutureCombine {
public static void main(String[] args) {
CompletableFuture<Double> price = CompletableFuture.supplyAsync(() -> 1200.0); // e.g. a product lookup
CompletableFuture<Double> taxRate = CompletableFuture.supplyAsync(() -> 0.18); // e.g. a tax service
CompletableFuture<Double> total = price.thenCombine(taxRate,
(p, rate) -> p + p * rate); // runs once both results are ready
System.out.println("Price: " + price.join());
System.out.println("Tax rate: " + taxRate.join());
System.out.println("Total: " + total.join());
}
}Output
Core Logic
Two independent tasks start at once, and thenCombine declares what to do when both have finished — no manual thread handling required.
- 1
CompletableFuture.supplyAsync(...)runs each lookup on a background thread from the common pool. - 2
price.thenCombine(taxRate, (p, rate) -> p + p * rate)creates a third future that runs once both inputs are ready. - 3
join()waits for a future and returns its value. - 4Main prints the inputs and the combined total.
Key Point: Because the two lookups run in parallel, the total is ready after the slower of the two — not after both added together, as it would be if they ran one after the other.
Key Concepts
Approach 2: Chaining Steps and Handling Errors
import java.util.concurrent.CompletableFuture;
public class CompletableFutureChain {
public static void main(String[] args) {
String ok = CompletableFuture.supplyAsync(() -> " order-1042 ")
.thenApply(String::trim) // each step transforms the previous result
.thenApply(String::toUpperCase)
.join();
System.out.println("Processed: " + ok);
String fallback = CompletableFuture.supplyAsync(CompletableFutureChain::callFlakyService)
.exceptionally(ex -> "default value") // recover instead of failing
.join();
System.out.println("Recovered: " + fallback);
}
static String callFlakyService() {
throw new IllegalStateException("service down"); // simulates a remote call that fails
}
}
Output
Core Logic
thenApply transforms a result step by step, and exceptionally supplies a fallback value if any earlier step fails.
- 1The first pipeline trims and upper-cases an order ID with two
thenApplysteps. - 2The second pipeline calls
callFlakyService, which throws an exception. - 3
exceptionally(ex -> "default value")catches that failure and replaces it with a fallback. - 4
join()returns the fallback instead of throwing.
" order-1042 " becomes ORDER-1042, and the failing call recovers to default value.Key Point: Without exceptionally (or handle), join() would throw a CompletionException wrapping the original error.
Key Concepts
Interview Questions
What's the difference between thenApply and thenCompose?
thenApply transforms a result with a normal function. thenCompose is used when the function itself returns another CompletableFuture, so the result isn't nested.
Which thread pool does supplyAsync use?
By default the shared ForkJoinPool.commonPool(). Pass your own Executor as a second argument for blocking or long-running work.