CompletableFuture - thenApply vs thenCompose
thenApply and thenCompose are two ways to chain work after a CompletableFuture<T> completes. thenApply takes a function T → U and returns CompletableFuture<U>. thenCompose takes a function T → CompletableFuture<U> and returns CompletableFuture<U> by "flattening" the nested future. Use thenApply for synchronous mapping; use thenCompose when the next step is async and returns a future.
Overview
When you chain asynchronous operations, you often need to:
- Transform the result of one future (sync) → use
thenApply - Call another async operation that returns a future (async) → use
thenCompose
If you use thenApply with a function that returns a CompletableFuture, you get CompletableFuture<CompletableFuture<U>> — a nested future. thenCompose flattens this to CompletableFuture<U>.
Rule of thumb: If the next step returns a value → thenApply. If the next step returns a CompletableFuture → thenCompose.
Example
Example 1: thenApply — synchronous transformation
JavaCompletableFuture<String> f = CompletableFuture.supplyAsync(() -> "hello"); CompletableFuture<Integer> g = f.thenApply(s -> s.length()); // g completes with 5. The lambda returns Integer, not Future.
The function s -> s.length() is synchronous. It returns a plain value. thenApply wraps that value in a new CompletableFuture<Integer>.
Example 2: thenCompose — chaining async calls
JavaCompletableFuture<Order> orderFuture = getOrderId() .thenCompose(id -> fetchOrder(id)); // fetchOrder(id) returns CompletableFuture<Order> // Without thenCompose: CompletableFuture<CompletableFuture<Order>> // With thenCompose: CompletableFuture<Order>
fetchOrder(id) is async and returns CompletableFuture<Order>. Using thenApply would give CompletableFuture<CompletableFuture<Order>>. thenCompose subscribes to the inner future and flattens the result.
Example 3: Wrong use of thenApply with async
Java// BAD: nested future CompletableFuture<CompletableFuture<User>> bad = getUserId() .thenApply(id -> fetchUser(id)); // To get User you'd need: bad.thenCompose(inner -> inner) // GOOD: flat future CompletableFuture<User> good = getUserId() .thenCompose(id -> fetchUser(id));
Example 4: Combining thenApply and thenCompose
JavaCompletableFuture<String> result = getUserId() .thenCompose(id -> fetchUser(id)) // async: id → User .thenApply(user -> user.getName()); // sync: User → String
Example 5: thenCombine — combining two futures
JavaCompletableFuture<String> a = CompletableFuture.supplyAsync(() -> "Hello"); CompletableFuture<String> b = CompletableFuture.supplyAsync(() -> "World"); CompletableFuture<String> combined = a.thenCombine(b, (x, y) -> x + " " + y); // combined completes with "Hello World"
thenCombine runs when both futures complete and applies a function to combine their results.
Example 6: allOf — wait for multiple futures
JavaCompletableFuture<Void> all = CompletableFuture.allOf( fetchUser(1), fetchUser(2), fetchUser(3) ); all.join(); // blocks until all complete
| Method | Argument | Result | Use case |
|---|---|---|---|
| thenApply | T → U | CompletableFuture<U> | Sync transformation |
| thenCompose | T → CompletableFuture<U> | CompletableFuture<U> | Chain async operation |
| thenCombine | (T, U) → V (with other future) | CompletableFuture<V> | Combine two futures |
Core Mechanism / Behavior
- thenApply: Registers a callback. When the source future completes, the callback runs and its return value becomes the result of the new future. The callback executes on the default executor or a provided one.
- thenCompose: Registers a callback that returns a future. When the source completes, the callback runs and returns a future. The composed future completes when that inner future completes. No blocking; it just wires the completion.
- Exception handling: If the source future completes exceptionally, neither callback runs. Use
exceptionallyorhandleto recover.
Key Rules
- Use thenApply when the continuation returns a plain value. Use thenCompose when it returns a
CompletableFutureto avoid nesting. - Prefer
thenComposefor "async A, then async B with A's result" (e.g. get order ID, then fetch order). UsethenApplyfor "async A, then sync transform of A's result." - Always handle exceptions with
exceptionallyorhandleso that failures do not go unnoticed.
What's Next
See Thread Pools and Core Parameters for choosing executors. See Volatile and Guarantees for visibility when sharing state between async stages.