The Problem: Blocking for Results
When you submit a task to a thread pool, you need a way to get the result. Without a placeholder, you’d have to block the calling thread until the work completes:
// Without Future — calling thread is stuck until work finishes
Result result = doExpensiveWork(); // Blocks here for 5 seconds
useResult(result);
Future and CompletableFuture solve this by giving you a handle to a result that will arrive later.
Future (Java 5+)
Future<V> represents the result of an asynchronous computation. You submit a task to an ExecutorService and get a Future back immediately:
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<String> future = executor.submit(() -> {
Thread.sleep(2000); // Simulate slow work
return "Result from background task";
});
// Do other work while task runs in background...
System.out.println("Doing other work...");
// When you need the result — blocks until available
String result = future.get(); // Blocks up to 2 seconds
System.out.println(result);
Future API
| Method | Description |
|---|---|
get() | Blocks until result is available, then returns it |
get(long timeout, TimeUnit unit) | Blocks with timeout. Throws TimeoutException if expired. |
isDone() | Returns true if task completed (success, failure, or cancellation) |
isCancelled() | Returns true if task was cancelled |
cancel(boolean mayInterrupt) | Attempts to cancel the task |
Limitations of Future
Future<String> future = executor.submit(() -> fetchFromDatabase());
// Problem 1: get() blocks the calling thread
String result = future.get(); // Thread does nothing while waiting
// Problem 2: No way to chain operations without blocking
String transformed = result.toUpperCase(); // Must block to get result first
// Problem 3: No way to combine multiple futures non-blocking
Future<String> future1 = executor.submit(() -> fetchUser());
Future<String> future2 = executor.submit(() -> fetchOrders());
// How to combine when BOTH are done? Only by blocking on each:
String user = future1.get(); // Block
String orders = future2.get(); // Block
| Limitation | Description |
|---|---|
Blocking get() | Forces you to wait — negates the async benefit |
| No chaining | Can’t say “when done, do this next” without blocking |
| No combining | Can’t merge results of multiple futures declaratively |
| No exception handling pipeline | Errors must be caught at get() call site |
| Cannot be manually completed | No way to set a result from outside |
CompletableFuture (Java 8+)
CompletableFuture<T> fixes all of these limitations. It supports non-blocking composition, chaining, and pipeline-style error handling:
import java.util.concurrent.CompletableFuture;
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Runs on ForkJoinPool.commonPool() by default
return fetchFromDatabase();
});
// Non-blocking chain — executes WHEN the result arrives
future
.thenApply(result -> result.toUpperCase()) // Transform
.thenApply(upper -> "Processed: " + upper) // Transform again
.thenAccept(final_ -> System.out.println(final_)) // Consume
.exceptionally(ex -> { // Handle errors
System.err.println("Failed: " + ex.getMessage());
return null;
});
Creating CompletableFutures
// Run async computation that returns a value
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> "hello");
// Run async computation with no return value
CompletableFuture<Void> cf2 = CompletableFuture.runAsync(() -> doSomething());
// Use custom executor instead of common pool
ExecutorService myPool = Executors.newFixedThreadPool(8);
CompletableFuture<String> cf3 = CompletableFuture.supplyAsync(() -> "hello", myPool);
// Create already-completed futures
CompletableFuture<String> cf4 = CompletableFuture.completedFuture("instant");
// Create and complete manually later
CompletableFuture<String> cf5 = new CompletableFuture<>();
// ... later in another thread:
cf5.complete("done!"); // Any thread can complete it
Transformation Methods (Non-Blocking Chaining)

| Method | Input → Output | Description |
|---|---|---|
thenApply(Function) | T → U | Transform the result |
thenAccept(Consumer) | T → void | Consume the result |
thenRun(Runnable) | void → void | Run action after completion |
thenCompose(Function) | T → CompletableFuture<U> | Flat-map (avoid nested futures) |
handle(BiFunction) | (T, Throwable) → U | Transform result OR error |
whenComplete(BiConsumer) | (T, Throwable) → void | Side-effect on completion |
exceptionally(Function) | Throwable → T | Recover from error |
thenApply vs thenCompose
// thenApply — synchronous transformation
CompletableFuture<String> name = fetchUser()
.thenApply(user -> user.getName()); // getName() is instant
// thenCompose — async transformation (returns another future)
CompletableFuture<List<Order>> orders = fetchUser()
.thenCompose(user -> fetchOrdersForUser(user.getId())); // fetchOrders is async
// Without thenCompose, you'd get CompletableFuture<CompletableFuture<List<Order>>>
// thenCompose "flattens" the nested future — similar to flatMap in streams
Combining Multiple Futures
CompletableFuture<String> userFuture = fetchUserAsync(userId);
CompletableFuture<List<Order>> ordersFuture = fetchOrdersAsync(userId);
CompletableFuture<Double> balanceFuture = fetchBalanceAsync(userId);
// Combine two futures
CompletableFuture<String> combined = userFuture.thenCombine(ordersFuture,
(user, orders) -> user.getName() + " has " + orders.size() + " orders"
);
// Wait for ALL futures to complete
CompletableFuture<Void> all = CompletableFuture.allOf(
userFuture, ordersFuture, balanceFuture
);
all.thenRun(() -> {
// All three are done — safe to call .join() without blocking
String user = userFuture.join();
List<Order> orders = ordersFuture.join();
Double balance = balanceFuture.join();
// Assemble response
});
// Wait for ANY future to complete (first one wins)
CompletableFuture<Object> fastest = CompletableFuture.anyOf(
fetchFromServer1(), fetchFromServer2(), fetchFromServer3()
);
Error Handling
CompletableFuture<String> pipeline = fetchData()
.thenApply(data -> transform(data))
.thenApply(transformed -> format(transformed))
.exceptionally(ex -> {
// Catches any exception from ANY stage above
logger.error("Pipeline failed", ex);
return "default value"; // Recovery value
});
// handle() — process both success and failure
CompletableFuture<String> robust = fetchData()
.handle((result, error) -> {
if (error != null) {
return "fallback";
}
return result.toUpperCase();
});
Async Variants
Every chaining method has an Async variant that runs on a different thread:
// Runs transformation on the SAME thread that completed the previous stage
future.thenApply(x -> transform(x));
// Runs transformation on the common ForkJoinPool
future.thenApplyAsync(x -> transform(x));
// Runs transformation on a custom executor
future.thenApplyAsync(x -> transform(x), myExecutor);
Use async variants when the transformation is CPU-intensive and you don’t want to block the completing thread.
Practical Example: Aggregating API Calls
public CompletableFuture<TripPage> buildTripPage(String tripId) {
CompletableFuture<Weather> weather = fetchWeatherAsync(tripId);
CompletableFuture<List<Restaurant>> restaurants = fetchRestaurantsAsync(tripId);
CompletableFuture<List<Event>> events = fetchEventsAsync(tripId);
return CompletableFuture.allOf(weather, restaurants, events)
.thenApply(ignored -> new TripPage(
weather.join(),
restaurants.join(),
events.join()
))
.orTimeout(3, TimeUnit.SECONDS) // Fail if takes > 3s (Java 9+)
.exceptionally(ex -> TripPage.fallback());
}
All three API calls run in parallel. When all complete, the results are assembled into a TripPage. If any call takes more than 3 seconds, the whole operation fails with a timeout and falls back to a default page.
Future vs CompletableFuture Summary
| Feature | Future | CompletableFuture |
|---|---|---|
| Get result | get() — blocks | get()/join() — blocks, OR chain non-blocking |
| Non-blocking chaining | ✗ | ✓ (thenApply, thenCompose, etc.) |
| Combine multiple | ✗ | ✓ (allOf, anyOf, thenCombine) |
| Error handling pipeline | ✗ | ✓ (exceptionally, handle) |
| Manually completable | ✗ | ✓ (complete(), completeExceptionally()) |
| Timeout support | Only via get(timeout) | orTimeout(), completeOnTimeout() (Java 9+) |
| Cancel | cancel() — best effort | cancel() + completeExceptionally() |
| Introduced | Java 5 | Java 8 |
When to Use Which
| Scenario | Recommendation |
|---|---|
| Simple “fire and forget” + get result later | Future is sufficient |
| Chain of transformations | CompletableFuture |
| Multiple parallel calls + combine | CompletableFuture.allOf/anyOf |
| Callback-based APIs | CompletableFuture (manually completable) |
| Legacy code using ExecutorService | Future (natural fit) |
Summary
Futureis a read-only handle that forces you to block onget().CompletableFutureis the modern approach — chainable, combinable, and non-blocking.- Use
thenApplyfor sync transforms,thenComposefor async transforms (flat-map). - Use
allOfto wait for all parallel tasks,anyOffor the first to complete. - Error handling flows through the pipeline with
exceptionallyandhandle. - With Java 21 virtual threads, the blocking nature of
Future.get()is less costly — butCompletableFuturestill wins for readable async pipelines.