What is the difference between Future and CompletableFuture in Java?

Java Future vs CompletableFuture
Quick Answer: 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 every get() 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) and completeExceptionally(ex) let external code drive the future.
  • Combinators: thenCombine, allOf, anyOf for coordinating multiple futures.
  • Timeouts (Java 9+): orTimeout() and completeOnTimeout().

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>
SinceJava 5Java 8
Get resultget() — always blocksget() or non-blocking callbacks
ChainingNot supportedthenApply, thenCompose, thenRun
Combining futuresManual onlythenCombine, allOf, anyOf
Exception handlingtry/catch around get()exceptionally, handle, whenComplete
Manual completionNot possiblecomplete(), completeExceptionally()
Timeout (Java 9+)get(timeout, unit) onlyorTimeout(), completeOnTimeout()
Async executionVia ExecutorServicesupplyAsync / 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 Future and 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) and thenCompose (async chain / flat-map).
  • How CompletableFuture.allOf() enables fork-join style parallelism.
  • The relationship to reactive frameworks like Project Reactor (Mono, Flux) — CompletableFuture is the standard library precursor.
  • Thread pool usage: by default supplyAsync uses ForkJoinPool.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.

Subscribe to Our Newsletter

Get the latest updates and exclusive content delivered to your inbox!