What is the difference between Future and CompletableFuture in Java?
Table of Contents
Future is a handle for an async result but forces you to block on get(). CompletableFuture (Java 8+) adds non-blocking chaining, combining, and pipeline-style exception handling — making it the modern choice for async programming.
1. Short Answer
Future<V>, introduced in Java 5, represents the result of an asynchronous computation. The core problem with Future is that the only way to retrieve the result is get(), which blocks the calling thread until the result is available. You cannot attach callbacks, chain dependent tasks, or combine results non-blocking.
CompletableFuture<T>, introduced in Java 8, implements both Future and CompletionStage. It allows you to:
- Chain asynchronous stages without blocking.
- Combine multiple futures (
thenCombine,allOf,anyOf). - Handle exceptions inline in the pipeline.
- Complete the future manually (
complete(),completeExceptionally()).
2. Future and Its Limitations
The Future interface has just five methods:
public interface Future {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
The critical limitations of Future:
- Blocking get(): The only way to retrieve a result is to block the caller thread — you cannot register a callback.
- No chaining: You cannot say "when this finishes, do that" without calling
get()and creating a new task manually. - No manual completion: You cannot programmatically set the result from outside the thread that ran the task.
- Poor exception handling: Exceptions are wrapped in
ExecutionException, requiring verbose try/catch around everyget()call. - No combination: No built-in way to run two futures in parallel and combine their results when both finish.
Blocking is a Performance Problem
If you call future.get() on a thread-pool thread, you are consuming a thread purely to wait. Under high concurrency this wastes threads and degrades throughput. CompletableFuture eliminates this waste with non-blocking callbacks.
3. CompletableFuture Overview
CompletableFuture was designed to overcome every limitation of Future. Key capabilities:
- supplyAsync / runAsync: Create async tasks using the common fork-join pool (or a custom executor).
- Non-blocking callbacks: Attach callbacks that run when the future completes — the calling thread is never blocked.
- Manual completion:
complete(value)andcompleteExceptionally(ex)let external code drive the future. - Combinators:
thenCombine,allOf,anyOffor coordinating multiple futures. - Timeouts (Java 9+):
orTimeout()andcompleteOnTimeout().
4. Chaining: thenApply, thenCompose, thenCombine
4.1 thenApply — transform the result
Like map on a stream. Applies a Function<T,U> synchronously when the previous stage completes.
CompletableFuture future = CompletableFuture
.supplyAsync(() -> "hello")
.thenApply(s -> s.toUpperCase()); // "HELLO"
4.2 thenCompose — chain async stages
Like flatMap on a stream. Use when the next step itself returns a CompletableFuture. Prevents nesting.
CompletableFuture future = CompletableFuture
.supplyAsync(() -> "user123")
.thenCompose(userId -> fetchUserFromDb(userId)); // returns CompletableFuture
// Result is CompletableFuture, NOT CompletableFuture>
4.3 thenCombine — combine two independent futures
Run two futures in parallel and combine their results when both complete.
CompletableFuture pricesFuture = CompletableFuture.supplyAsync(() -> fetchPrice());
CompletableFuture taxFuture = CompletableFuture.supplyAsync(() -> fetchTax());
CompletableFuture totalFuture = pricesFuture
.thenCombine(taxFuture, (price, tax) -> price + tax);
5. Exception Handling
CompletableFuture provides three methods for handling exceptions inline without try/catch:
// exceptionally — provide a fallback value on failure
CompletableFuture result = CompletableFuture
.supplyAsync(() -> { throw new RuntimeException("oops"); })
.exceptionally(ex -> "default-value");
// handle — handle both success and failure; always runs
CompletableFuture result2 = CompletableFuture
.supplyAsync(() -> "ok")
.handle((value, ex) -> {
if (ex != null) return "error: " + ex.getMessage();
return value.toUpperCase();
});
// whenComplete — observe result or exception without transforming
CompletableFuture result3 = CompletableFuture
.supplyAsync(() -> "data")
.whenComplete((value, ex) -> {
if (ex != null) System.out.println("Failed: " + ex.getMessage());
else System.out.println("Succeeded: " + value);
});
6. Code Example: Full Async Pipeline
This example fetches a user ID, then fetches that user's profile asynchronously, then formats a greeting — all without blocking any thread:
import java.util.concurrent.*;
public class CompletableFutureDemo {
// Simulates a remote call
static CompletableFuture fetchUserId() {
return CompletableFuture.supplyAsync(() -> {
sleep(200); // simulate network delay
return "user-42";
});
}
// Simulates a DB lookup
static CompletableFuture fetchUserName(String userId) {
return CompletableFuture.supplyAsync(() -> {
sleep(300);
return "Alice";
});
}
public static void main(String[] args) throws Exception {
// --- OLD WAY: Future (blocking) ---
ExecutorService executor = Executors.newFixedThreadPool(2);
Future f = executor.submit(() -> "user-42");
String userId = f.get(); // BLOCKS the main thread here
System.out.println("Future userId: " + userId);
executor.shutdown();
// --- NEW WAY: CompletableFuture (non-blocking pipeline) ---
CompletableFuture pipeline = fetchUserId() // Stage 1
.thenCompose(uid -> fetchUserName(uid)) // Stage 2 (async chain)
.thenApply(name -> "Hello, " + name + "!") // Stage 3 (transform)
.exceptionally(ex -> "Hello, Guest! (error: " + ex.getMessage() + ")");
// Non-blocking callback — called when pipeline completes
pipeline.thenAccept(greeting -> System.out.println("Greeting: " + greeting));
// Wait for demo (in production, your framework waits instead)
pipeline.get();
// Combine two parallel futures
CompletableFuture priceFuture = CompletableFuture.supplyAsync(() -> { sleep(100); return 100; });
CompletableFuture taxFuture = CompletableFuture.supplyAsync(() -> { sleep(150); return 18; });
int total = priceFuture.thenCombine(taxFuture, Integer::sum).get();
System.out.println("Total (price + tax): " + total); // 118
// Wait for ALL futures
CompletableFuture all = CompletableFuture.allOf(
CompletableFuture.runAsync(() -> System.out.println("Task A")),
CompletableFuture.runAsync(() -> System.out.println("Task B")),
CompletableFuture.runAsync(() -> System.out.println("Task C"))
);
all.get(); // waits for all three
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
// Output:
// Future userId: user-42
// Task A / Task B / Task C (in any order)
// Greeting: Hello, Alice!
// Total (price + tax): 118
7. Key Differences Table
| Feature | Future<V> | CompletableFuture<T> |
|---|---|---|
| Since | Java 5 | Java 8 |
| Get result | get() — always blocks | get() or non-blocking callbacks |
| Chaining | Not supported | thenApply, thenCompose, thenRun |
| Combining futures | Manual only | thenCombine, allOf, anyOf |
| Exception handling | try/catch around get() | exceptionally, handle, whenComplete |
| Manual completion | Not possible | complete(), completeExceptionally() |
| Timeout (Java 9+) | get(timeout, unit) only | orTimeout(), completeOnTimeout() |
| Async execution | Via ExecutorService | supplyAsync / runAsync (built-in) |
8. When to Use Each
Use Future when:
- You have a single, simple task and are OK blocking the caller thread to retrieve the result.
- You are working with older APIs that return
Futureand cannot be changed.
Use CompletableFuture when:
- You need to chain multiple async operations without blocking intermediate steps.
- You need to run tasks in parallel and combine their results.
- You need timeout handling or pipeline exception handling.
- You are building reactive-style code where threads should not block while waiting for I/O.
9. Why This Question Matters in Interviews
This question probes your depth in modern Java concurrency. Senior developers are expected to know:
- Why
Future.get()is problematic in high-throughput systems. - The difference between
thenApply(sync transform) andthenCompose(async chain / flat-map). - How
CompletableFuture.allOf()enables fork-join style parallelism. - The relationship to reactive frameworks like Project Reactor (
Mono,Flux) —CompletableFutureis the standard library precursor. - Thread pool usage: by default
supplyAsyncusesForkJoinPool.commonPool(); always specify a custom executor for I/O-bound work.
10. Conclusion
Future is a simple placeholder for an async result but forces blocking retrieval and offers no composition capabilities. CompletableFuture is the modern replacement: it supports non-blocking pipelines, parallel task combination, and inline exception handling. In any greenfield Java 8+ code, prefer CompletableFuture. Understanding the difference — especially thenApply vs thenCompose and exception handling — is a reliable signal of production-level Java concurrency experience.