Java Programs›Multithreading›Combine Async Results with CompletableFuture

Combine Async Results with CompletableFuture in Java

advanced·  Multithreading  ·  Concurrency

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.

Input
price 1200.0, tax rate 0.18
Output
Total: 1416.0

Java Program

Java
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

Price: 1200.0 Tax rate: 0.18 Total: 1416.0

Core Logic

Two independent tasks start at once, and thenCombine declares what to do when both have finished — no manual thread handling required.

How It Works
  1. 1CompletableFuture.supplyAsync(...) runs each lookup on a background thread from the common pool.
  2. 2price.thenCombine(taxRate, (p, rate) -> p + p * rate) creates a third future that runs once both inputs are ready.
  3. 3join() waits for a future and returns its value.
  4. 4Main prints the inputs and the combined total.
1200.0 + 1200.0 × 0.18 = 1416.0.
💡

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

supplyAsync()thenCombine()join()

Approach 2: Chaining Steps and Handling Errors

Java
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

Processed: ORDER-1042 Recovered: default value

Core Logic

thenApply transforms a result step by step, and exceptionally supplies a fallback value if any earlier step fails.

How It Works
  1. 1The first pipeline trims and upper-cases an order ID with two thenApply steps.
  2. 2The second pipeline calls callFlakyService, which throws an exception.
  3. 3exceptionally(ex -> "default value") catches that failure and replaces it with a fallback.
  4. 4join() 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

thenApply()exceptionally()method reference

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.

Learn the Concept

Related Programs