Home/Learn/Java A–Z/CompletableFuture

CompletableFuture

Advanced
Concurrency

CompletableFuture enables non-blocking async pipelines — chain transformations, combine results, and handle errors without blocking threads.

Overview

CompletableFuture (Java 8) is a powerful async framework built on Future. Unlike Future.get() which always blocks, CompletableFuture supports callback-based chaining: thenApply (transform result), thenCompose (chain dependent futures), thenCombine (combine two independent futures), allOf (wait for all), anyOf (first to complete). It also supports exception handling with exceptionally() and handle(). By default, callbacks run on the ForkJoinPool.commonPool().

Async Execution and Basic Chaining

CompletableFuture.supplyAsync() runs a Supplier asynchronously. thenApply() transforms the result (like map). thenAccept() consumes it without returning. thenRun() runs an action with no access to the result.

All these methods return a new CompletableFuture, enabling fluent chaining.

BasicChaining.java
import java.util.concurrent.CompletableFuture;

// Run async, transform result
CompletableFuture<String> cf = CompletableFuture
    .supplyAsync(() -> fetchUserFromDb(42))          // async
    .thenApply(user -> user.getEmail())              // transform
    .thenApply(String::toUpperCase);                 // transform again

// Non-blocking callback
cf.thenAccept(email ->
    System.out.println("Email: " + email));

// Block only at the end if you need the value
String email = cf.join(); // like get() but throws unchecked

// Run with specific executor (avoid hogging common pool)
ExecutorService ioPool = Executors.newFixedThreadPool(10);

CompletableFuture<String> withPool = CompletableFuture
    .supplyAsync(() -> callExternalApi(), ioPool)
    .thenApplyAsync(response -> parse(response), ioPool);

Combining Futures

thenCompose() chains dependent futures (flatMap — second depends on first result). thenCombine() combines two independent futures when both complete.

allOf() waits for all futures to complete; anyOf() completes when the first one does.

Combining.java
// thenCompose — sequential dependent async calls (flatMap)
CompletableFuture<Order> orderFuture = CompletableFuture
    .supplyAsync(() -> getUserId("alice"))
    .thenCompose(userId ->                        // userId needed for next call
        CompletableFuture.supplyAsync(() -> getLatestOrder(userId)));

// thenCombine — two independent calls, combine results
CompletableFuture<String> userFuture  =
    CompletableFuture.supplyAsync(() -> fetchUser(1));
CompletableFuture<String> orderFuture2 =
    CompletableFuture.supplyAsync(() -> fetchOrder(1));

CompletableFuture<String> combined = userFuture.thenCombine(
    orderFuture2,
    (user, order) -> user + " placed " + order);

// allOf — wait for all
CompletableFuture<Void> all = CompletableFuture.allOf(
    userFuture, orderFuture2);
all.thenRun(() -> System.out.println("Both done"));

// anyOf — first to complete wins
CompletableFuture<Object> first = CompletableFuture.anyOf(
    CompletableFuture.supplyAsync(() -> fetchFromCache()),
    CompletableFuture.supplyAsync(() -> fetchFromDb()));

Error Handling

exceptionally() catches an exception and provides a fallback value (like catch). handle() is called regardless of success or failure — it receives the result and the exception (one will be null).

whenComplete() is like handle() but cannot transform the result — used for side effects like logging.

ErrorHandling.java
CompletableFuture<String> cf = CompletableFuture
    .supplyAsync(() -> callExternalApi())     // may throw
    .thenApply(response -> parse(response))

    // Catch exception, return fallback
    .exceptionally(ex -> {
        log.error("API failed: " + ex.getMessage());
        return "fallback-value";
    });

// handle() — always called, can transform both success and error
CompletableFuture<String> robust = CompletableFuture
    .supplyAsync(() -> callApi())
    .handle((result, ex) -> {
        if (ex != null) {
            return "error: " + ex.getCause().getMessage();
        }
        return result.toUpperCase();
    });

// Timeout (Java 9+)
CompletableFuture<String> withTimeout = CompletableFuture
    .supplyAsync(() -> slowOperation())
    .orTimeout(3, TimeUnit.SECONDS)           // completes exceptionally if slow
    .exceptionally(ex -> "timed out");

Interactive Visualization

NEWRUNNABLERUNNINGBLOCKEDWAITINGTERMINATED
synchronized(lock)— free
main
RUNNING
t1
NEW
t2
NEW
main thread creates Thread t1 and Thread t2. Both are in NEW state.
1 / 6

Key Points to Remember

  • supplyAsync() runs async; thenApply() transforms (map); thenCompose() chains (flatMap).
  • thenCombine() joins two independent futures; allOf() waits for all; anyOf() takes the first.
  • exceptionally() handles errors with a fallback; handle() processes both success and failure.
  • join() is like get() but throws unchecked — prefer it in lambda chains.
  • Always specify a custom executor for I/O tasks to avoid starving the common ForkJoinPool.

Practice CompletableFuture in the Playground

Run and modify code directly in your browser - no setup needed.

Interview Questions

Sign in to ask Aria
1

What is the difference between thenApply() and thenCompose()?

MediumAmazon
2

What is the difference between allOf() and anyOf()?

EasyGoogle
3

How does exceptionally() differ from handle()?

MediumOracle
4

Why should you avoid using the common ForkJoinPool for I/O-heavy CompletableFuture tasks?

HardNetflix
5

How would you implement a timeout for a CompletableFuture pipeline?

MediumMicrosoft

Ask Aria about CompletableFuture

Your personal AI tutor — ask anything about this concept