Question 3 · Section 19

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.

Language versions: English Russian Ukrainian

🟢 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: completedFuture creates 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]]