CompletableFuture
The Future from the previous lesson can do only one thing — wait for the result with get(). CompletableFuture goes further: you can attach the next steps to a future result in advance — "when it's loaded, transform it", "when both are ready, combine them". Such code is called asynchronous: tasks run in the background, and the main thread doesn't sit idle waiting for each of them.
The first asynchronous task
CompletableFuture.supplyAsync runs a task on a background thread and immediately returns a promise object. The join() method waits for the result:
void main() {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 6 * 7);
IO.println("Result: " + future.join());
}Output:
Result: 42join() is like get() from Future, but it doesn't require handling checked exceptions. By default tasks run in the shared thread pool ForkJoinPool.commonPool().
A chain of steps
thenApply transforms the result once it's ready — like map on streams. thenAccept receives the final value and returns nothing:
void pause(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
String loadName(int userId) {
pause(200);
return "anna";
}
void main() {
CompletableFuture<Void> done = CompletableFuture
.supplyAsync(() -> loadName(7))
.thenApply(name -> name.substring(0, 1).toUpperCase() + name.substring(1))
.thenApply(name -> "Hello, " + name + "!")
.thenAccept(text -> IO.println(text));
IO.println("Waiting for the load...");
done.join();
}Output:
Waiting for the load...
Hello, Anna!"Waiting for the load..." came first: the main thread didn't wait for the background task to load the name. The pause method hides the checked InterruptedException from Thread.sleep.
Combining two tasks
thenCombine waits for two independent tasks and combines their results. The tasks run in parallel:
void pause(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
void main() {
CompletableFuture<Double> price = CompletableFuture.supplyAsync(() -> {
pause(300);
return 20.0;
});
CompletableFuture<Double> rate = CompletableFuture.supplyAsync(() -> {
pause(300);
return 0.9;
});
double total = price.thenCombine(rate, (p, r) -> p * r).join();
IO.println("Price in euros: " + total);
}Output:
Price in euros: 18.0Both 300 ms tasks ran at the same time, so the total is ready after about 300 ms, not 600.
Many tasks
CompletableFuture.allOf returns a promise that completes when all the given tasks finish. After that the results can be collected with join() in the order you need:
void main() {
List<String> cities = List.of("Rome", "Oslo", "Lima");
List<CompletableFuture<String>> futures = cities.stream()
.map(city -> CompletableFuture.supplyAsync(() -> city + ": " + city.length() * 5 + "°"))
.toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
for (CompletableFuture<String> f : futures) {
IO.println(f.join());
}
}Output:
Rome: 20°
Oslo: 20°
Lima: 20°Handling errors
If a task in the chain throws an exception, the following thenApply steps are skipped, and exceptionally lets you substitute a fallback value:
void main() {
String result = CompletableFuture
.supplyAsync(() -> Integer.parseInt("hundred"))
.thenApply(n -> "Number: " + n)
.exceptionally(e -> "Error: " + e.getCause().getMessage())
.join();
IO.println(result);
}Output:
Error: For input string: "hundred"The exception reaches exceptionally wrapped in a CompletionException, so the original error is obtained with getCause().
Common mistake
If you don't wait for the result, the program may end before the background task. Threads of the shared pool are daemon threads: the JVM doesn't wait for them when main exits:
void pause(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
void main() {
CompletableFuture.runAsync(() -> {
pause(500);
IO.println("Report saved");
});
IO.println("Program finished");
}Output:
Program finishedThe "Report saved" line never appeared. Keep the promise in a variable and call join() on it before exiting.