Future and CompletableFuture in Java
A Future is a placeholder for a result that is not ready yet. CompletableFuture adds composition, so results can be chained without blocking.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
Future
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<Integer> future = executor.submit(() -> {
Thread.sleep(500);
return 42;
});
System.out.println(future.isDone()); // probably false
Integer result = future.get(); // blocks until ready
Integer withTimeout = future.get(2, TimeUnit.SECONDS);
future.cancel(true); // interrupt if still runningWhat Future cannot do
- It cannot tell you when it completes; you must poll or block.
- It cannot be chained to another computation.
- It cannot be combined with a second future.
- It has no way to express failure other than throwing from
get.
CompletableFuture, added in Java 8, addresses all four.
Creating a CompletableFuture
CompletableFuture<String> async = CompletableFuture.supplyAsync(() -> fetch(url));
CompletableFuture<Void> task = CompletableFuture.runAsync(() -> cleanUp());
CompletableFuture<String> ready = CompletableFuture.completedFuture("cached");
CompletableFuture<String> onMyPool =
CompletableFuture.supplyAsync(() -> fetch(url), executor);Without an explicit executor these run on the common ForkJoin pool, which is sized for CPU work and shared with parallel streams. Always pass your own executor for anything that blocks.
Chaining
CompletableFuture<Integer> pipeline = CompletableFuture
.supplyAsync(() -> fetch(url), executor) // String
.thenApply(String::strip) // transform
.thenApply(String::length); // transform again
System.out.println(pipeline.join());
| Method | The function | Produces |
|---|---|---|
thenApply | Takes the value, returns a value | CompletableFuture<R> |
thenAccept | Takes the value, returns nothing | CompletableFuture<Void> |
thenRun | Takes nothing | CompletableFuture<Void> |
thenCompose | Returns another future | A flattened future |
thenCombine | Merges two independent futures | One future |
thenApply against thenCompose
// Nested, which is rarely what you want
CompletableFuture<CompletableFuture<Note>> nested =
findId(slug).thenApply(id -> loadNote(id));
// Flattened
CompletableFuture<Note> flat =
findId(slug).thenCompose(id -> loadNote(id));This is the same distinction as map and flatMap on a stream: use thenCompose when the function itself returns a future.
Combining independent work
CompletableFuture<Profile> profile = CompletableFuture.supplyAsync(this::loadProfile, executor);
CompletableFuture<List<Note>> notes = CompletableFuture.supplyAsync(this::loadNotes, executor);
CompletableFuture<Dashboard> dashboard =
profile.thenCombine(notes, Dashboard::new); // both run at once
Dashboard result = dashboard.join();List<CompletableFuture<String>> futures = urls.stream()
.map(url -> CompletableFuture.supplyAsync(() -> fetch(url), executor))
.toList();
CompletableFuture<Void> all = CompletableFuture
.allOf(futures.toArray(new CompletableFuture[0]));
List<String> results = all.thenApply(ignored ->
futures.stream().map(CompletableFuture::join).toList()).join();allOf completes when every future does; anyOf completes with the first. Note that allOf returns Void, so the results are collected afterwards as above.
Handling failure
CompletableFuture<String> safe = CompletableFuture
.supplyAsync(() -> fetch(url), executor)
.exceptionally(error -> {
logger.warning("Fetch failed: " + error.getMessage());
return ""; // a fallback value
});
CompletableFuture<String> handled = CompletableFuture
.supplyAsync(() -> fetch(url), executor)
.handle((value, error) -> error == null ? value : "fallback");
CompletableFuture<String> observed = CompletableFuture
.supplyAsync(() -> fetch(url), executor)
.whenComplete((value, error) -> log(value, error)); // observes, changes nothing| Method | Runs on | Can replace the result |
|---|---|---|
exceptionally | Failure only | Yes |
handle | Both | Yes |
whenComplete | Both | No |
An exception inside a stage is wrapped in aCompletionException. CallgetCause()to reach the original, or the log will only ever show the wrapper.
get compared with join
future.get(); // throws checked InterruptedException and ExecutionException
future.join(); // throws unchecked CompletionExceptionjoin is usually more convenient inside a lambda, because the built in functional interfaces cannot declare checked exceptions.
Async variants
.thenApply(this::transform) // may run on the completing thread
.thenApplyAsync(this::transform) // runs on the common pool
.thenApplyAsync(this::transform, executor) // runs on a pool you controlThe plain form may execute on whichever thread completed the previous stage. When a stage does anything slow, use the Async variant with an explicit executor so a caller thread is never hijacked.
A worked example
public CompletableFuture<String> buildSummary(long noteId) {
return CompletableFuture
.supplyAsync(() -> repository.load(noteId), executor)
.thenCompose(note -> CompletableFuture
.supplyAsync(() -> author.load(note.authorId()), executor)
.thenApply(name -> note.title() + " by " + name))
.orTimeout(3, TimeUnit.SECONDS)
.exceptionally(error -> "Summary unavailable");
}orTimeout and completeOnTimeout, added in Java 9, put a bound on a stage without any extra scheduling code.
Structured concurrency
// Preview in recent releases; check availability for your version
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var profile = scope.fork(this::loadProfile);
var notes = scope.fork(this::loadNotes);
scope.join();
scope.throwIfFailed();
return new Dashboard(profile.get(), notes.get());
}Structured concurrency ties the lifetime of subtasks to a scope: if one fails the rest are cancelled, and nothing outlives the block. It is the direction modern Java concurrency is heading, and it pairs naturally with virtual threads.
Common mistakes
- Calling
get()immediately after creating the future, which makes it synchronous. - Using the common pool for blocking work and starving parallel streams.
- Using
thenApplywherethenComposeis needed, producing a nested future. - Forgetting error handling, so a failure disappears silently.
- Logging a
CompletionExceptionwithout unwrapping the cause. - Assuming
allOfreturns the results; it returnsVoid.
Best practices
- Pass an explicit executor for anything that blocks.
- Compose stages rather than blocking between them.
- Always terminate a chain with
exceptionallyorhandle. - Use
thenComposefor dependent work andthenCombinefor independent work. - Set a timeout with
orTimeout. - Unwrap
CompletionExceptionbefore logging.
Practice
- Fetch two independent values concurrently and combine them into one object.
- Explain why
thenApplyreturns a nested future when the function returns a future. - Add a fallback so a failing call returns a default instead of propagating.
- Why is
joinoften easier thangetinside a lambda? - Convert a chain of blocking calls into a non blocking pipeline and describe what changed.
Conclusion
A Future holds a pending result; a CompletableFuture lets you describe what happens next without blocking. Compose rather than wait, always handle failure, and give blocking work its own executor.