Composing CompletableFuture

Multithreading · lesson 11 of 38 · 4 min read

Chain and combine async steps without blocking, and recover cleanly when one of them fails.

Open this lesson in the learning hub

Key points

  • supplyAsync starts work and returns immediately. Nothing blocks until you ask for the value.
  • thenApply transforms a result. thenCompose is flatMap: use it when the next step is itself async, or you get a nested future.
  • thenCombine merges two independent futures so both run in parallel. allOf waits for a whole batch.
  • exceptionally and handle recover from failure. The exception arrives wrapped in CompletionException, so unwrap with getCause().
  • The default executor is the common ForkJoinPool. Never do blocking I/O there. Pass your own executor to the *Async overloads.
  • join() is get() without checked exceptions. Call it once, at the very edge of your code.

Example

import java.util.concurrent.CompletableFuture;

public class Main {
    public static void main(String[] args) {
        CompletableFuture<String> user   = CompletableFuture.supplyAsync(() -> { sleep(100); return "ada"; });
        CompletableFuture<Integer> count = CompletableFuture.supplyAsync(() -> { sleep(100); return 3; });

        // two independent calls, run in parallel, merged at the end
        System.out.println(user.thenCombine(count, (u, c) -> u + " has " + c + " orders").join());

        // dependent steps: transform, then call something that is also async
        String greeting = CompletableFuture.supplyAsync(() -> "ada")
                .thenApply(String::toUpperCase)
                .thenCompose(name -> CompletableFuture.supplyAsync(() -> "hello " + name))
                .join();
        System.out.println(greeting);

        // recover instead of blowing up
        String safe = CompletableFuture.<String>supplyAsync(() -> { throw new IllegalStateException("timeout"); })
                .exceptionally(ex -> "fallback: " + ex.getCause().getMessage())
                .join();
        System.out.println(safe);
    }

    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

Describe the whole pipeline first, then block exactly once, at the end.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Multithreading course, and every lesson in it is listed on the Multithreading contents page.