CompletableFuture
Compose asynchronous pipelines with CompletableFuture for non-blocking concurrent code.
Overview
Key Points
- •supplyAsync(() -> value) starts an async task that returns a result
- •runAsync(() -> {}) starts an async task with no result
- •thenApply(fn) transforms the result (like Stream.map)
- •thenAccept(fn) consumes the result without returning a new value
- •thenCompose(fn) chains another CompletableFuture (flatMap equivalent)
- •thenCombine(other, fn) waits for two futures and merges results
- •allOf(cf1, cf2, ...) waits for all futures to complete
- •anyOf(cf1, cf2, ...) completes when the first future finishes
- •exceptionally(fn) handles errors inline without try-catch
- •join() blocks and retrieves the result (throws unchecked exception on failure)
Examples
Basic async pipeline with thenApply and thenAccept
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> {
// Simulates fetching data from a remote source
return "raw data";
})
.thenApply(data -> data.toUpperCase()) // transform
.thenApply(data -> "[" + data + "]") // another transform
.thenAccept(result -> System.out.println("Result: " + result))
.join(); // wait for the pipeline to finish
}
}Each thenApply runs on a thread from the common pool (or whichever executor the previous stage used). join() blocks the calling thread until the whole chain completes.
Combining two independent futures with thenCombine
import java.util.concurrent.*;
public class Main {
static CompletableFuture<Integer> fetchPrice() {
return CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return 100;
});
}
static CompletableFuture<Integer> fetchDiscount() {
return CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(80); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return 20;
});
}
public static void main(String[] args) {
CompletableFuture<Integer> result = fetchPrice()
.thenCombine(fetchDiscount(), (price, discount) -> price - discount);
System.out.println("Final price: " + result.join()); // 80
}
}Both futures run in parallel. thenCombine merges their results only after both complete. Total wait is ~100ms, not 180ms, because they run concurrently.
Error handling with exceptionally
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
String result = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("fetch failed");
return "fetched data";
})
.exceptionally(ex -> "fallback: " + ex.getMessage())
.join();
System.out.println(result);
}
}exceptionally acts like a catch block. It only runs when the upstream stage threw an exception. The returned value becomes the new result for downstream stages.
Waiting for all futures with allOf
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<String> urls = List.of("url1", "url2", "url3");
List<CompletableFuture<String>> futures = urls.stream()
.map(url -> CompletableFuture.supplyAsync(() -> "fetched: " + url))
.collect(Collectors.toList());
CompletableFuture<Void> allDone =
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
List<String> results = allDone.thenApply(v ->
futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList())
).join();
results.forEach(System.out::println);
}
}allOf returns CompletableFuture<Void>. To collect results, use thenApply after allOf and call join() on each individual future (they are all done at that point so join() does not block).
Common Mistakes
- Calling get() instead of join() in a pipeline. get() throws checked exceptions that force awkward try-catch inside lambdas. join() throws unchecked CompletionException instead.
- Blocking inside supplyAsync. If you call Thread.sleep or another blocking call, you tie up a thread from the pool. Use thenCompose to chain the next async step instead.
- Ignoring the return value of thenApply and friends. Each method returns a new CompletableFuture. You must chain or store it; the original future is unaffected.
- Using allOf on a list of futures and then calling join() on them before allOf completes. Always obtain results inside the thenApply callback after allOf, or call join() after allOf.join().
Note
Supply a dedicated ExecutorService to supplyAsync and thenApplyAsync to isolate slow tasks from the common pool: CompletableFuture.supplyAsync(() -> slowCall(), myExecutor). This prevents one slow task from starving the pool shared by the whole application.
Exercises
Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.
Completed: 0 / 4 exercises
Knowledge Check
Question 1 of 4