How to create a CompletableFuture that is already completed with result
Sometimes you need to return an already completed CompletableFuture — for example, when data is in cache or for testing.
🟢 Junior Level
Sometimes you need to return an already completed CompletableFuture — for example, when data is in cache or for testing.
completedFuture(value)
// Instantly completed CF with a result
CompletableFuture<String> cf = CompletableFuture.completedFuture("Hello");
cf.thenAccept(s -> System.out.println(s)); // Hello — no delay
failedFuture(Throwable) — Java 9+
// Instantly completed CF with an error
CompletableFuture<String> cf = CompletableFuture.failedFuture(
new RuntimeException("Error")
);
cf.exceptionally(ex -> "fallback"); // will catch the error
Before Java 9 you had to do it manually:
CompletableFuture<String> cf = new CompletableFuture<>();
cf.completeExceptionally(new RuntimeException("Error"));
🟡 Middle Level
Why is this needed in real projects?
1. Conditional asynchrony (Caching):
public CompletableFuture<Data> getData(String id) {
Data cached = cache.get(id);
if (cached != null) {
return CompletableFuture.completedFuture(cached); // Instant response
}
return CompletableFuture.supplyAsync(() -> db.fetch(id)); // Async request
}
2. Fallback / Null Object:
return CompletableFuture.completedFuture(Collections.emptyList());
3. Mocking and testing:
// In unit tests — synchronous mode
when(service.getDataAsync("1")).thenReturn(
CompletableFuture.completedFuture(testData)
);
Critical nuance: Execution thread
If you call .thenApply() on an already completed CF:
- The callback will execute in the CURRENT thread (the one calling
thenApply) - If the CF is not yet completed, the callback will execute in the thread that completes the CF
Consequence: If you expected asynchrony but got an already completed CF, your “heavy” callback may block the main thread.
Solution: Use *Async versions of methods if you are unsure about the CF source.
// Safe: thenApplyAsync always switches thread
completedFuture.thenApplyAsync(data -> heavyTransform(data), executor);
// Dangerous: may execute in the current thread
completedFuture.thenApply(data -> heavyTransform(data));
completedStage(value) — Java 12+
// Returns a CompletionStage — cannot be completed manually
CompletionStage<String> stage = CompletableFuture.completedStage("Hello");
// stage.complete(...) — won't compile!
Immutable representation — safer for public APIs.
🔴 Senior Level
Performance and Highload
- Allocation:
completedFuturecreates a heap object. In extreme cases (millions of requests per sec) this can pressure the GC. - Project Valhalla: In the future CF may become a Value type (or be optimized via Scalar Replacement), eliminating allocation overhead. // Project Valhalla — under development, timeline not defined. // Do not rely on this in production solutions.
Edge Cases
1. Failed Future vs exceptionally:
failedFuture immediately puts the chain into an error state. All subsequent thenApply calls will be skipped.
2. Short-circuit:
If an exceptionally in the middle of a long chain returns a result, all subsequent thenApply calls will work with this result as if no error occurred.
Best Practices
// ✅ Use completedFuture for cache
if (cached != null) return CompletableFuture.completedFuture(cached);
// ✅ failedFuture for errors (Java 9+)
return CompletableFuture.failedFuture(new BusinessException("Not found"));
// ✅ completedStage for public APIs (Java 12+)
public CompletionStage<Data> getData() { ... }
// ❌ Don't block in callbacks of completed CFs
// ❌ Don't ignore that CF is already completed
🎯 Interview Cheat Sheet
Must know:
- completedFuture(value) — instantly completed CF with a result
- failedFuture(Throwable) — Java 9+, instantly completed CF with an error
- completedStage(value) — Java 12+, immutable CompletionStage
- completedFuture runs in the SAME thread — callbacks execute synchronously
- completedFuture is useful for cache, mocks, fallback, conditional async
Frequent follow-up questions:
- Why completedFuture if the result is already there? — For a unified async API: cache returns completedFuture, DB — supplyAsync
- completedFuture.thenApply — which thread will it run in? — In the current (calling) thread, since CF is already completed
- How to create a failed CF in Java 8? — new CompletableFuture() + completeExceptionally(ex)
- When to use completedStage instead of completedFuture? — For public APIs so the caller cannot complete manually
Red flags (DO NOT say):
- “completedFuture creates a new thread” — it is synchronous, no threads
- “completedFuture.thenApplyAsync is always safe” — Async switches threads, but overhead is excessive for lightweight operations
- “failedFuture and exceptionally are the same” — failedFuture creates a CF in error state, exceptionally handles an error on an existing one
Related topics:
- [[26. Can you manually complete a CompletableFuture with a result]]
- [[6. How to handle exceptions in CompletableFuture chain]]
- [[16. What does supplyAsync() method do and when to use it]]
- [[23. How to test code with CompletableFuture]]