What do thenAccept() and thenRun() methods do
Both methods are used for completing a chain of CompletableFuture:
🟢 Junior Level
Both methods are used for completing a chain of CompletableFuture:
thenAccept()— receives the result and consumes it (returns nothing)thenRun()— simply performs an action, without knowledge of the result
// thenAccept — receives the result
CompletableFuture.supplyAsync(() -> "Hello")
.thenAccept(s -> System.out.println("Got: " + s)); // Got: Hello
// thenRun — does not receive the result
CompletableFuture.supplyAsync(() -> "Hello")
.thenRun(() -> System.out.println("Done!")); // Done!
Simple analogy:
thenAccept— receive a package and open itthenRun— just know that the package was delivered
🟡 Middle Level
Detailed comparison
**thenAccept — Consumer
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "Hello");
// We get the result
cf.thenAccept(result -> {
System.out.println("Result: " + result);
saveToDatabase(result);
});
// Type: CompletableFuture<Void> — cannot continue chain with a value
thenRun — Runnable:
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "Hello");
// Just an action after completion
cf.thenRun(() -> {
System.out.println("Task completed");
notifyUser();
});
When to use which
thenAccept:
// When you need the result
fetchDataAsync()
.thenAccept(data -> {
process(data);
saveToCache(data);
});
thenRun:
// When the result doesn't matter
sendEmailAsync(email)
.thenRun(() -> log.info("Email sent"));
// Or for side effects
saveToDatabase(entity)
.thenRun(() -> metrics.increment("saved"));
Typical mistakes
- Trying to get the result in thenRun:
```java
CompletableFuture
cf = CompletableFuture.supplyAsync(() -> "Hello");
// ❌ thenRun does not receive the result cf.thenRun(() -> { // System.out.println(result); // result is not available! System.out.println(“Done”); });
// ✅ thenAccept receives the result cf.thenAccept(result -> System.out.println(result));
---
## 🔴 Senior Level
### Internal Implementation
**thenApply vs thenAccept vs thenRun:**
```java
public <U> CompletableFuture<U> thenApply(Function<T, U> fn) {
// fn: T -> U
// Returns CompletableFuture<U>
}
public CompletableFuture<Void> thenAccept(Consumer<T> action) {
// action: T -> void
// Returns CompletableFuture<Void>
}
public CompletableFuture<Void> thenRun(Runnable action) {
// action: () -> void
// Returns CompletableFuture<Void>
}
Void result:
// thenAccept and thenRun return CompletableFuture<Void>
// But you can continue the chain:
cf.thenAccept(result -> process(result))
.thenRun(() -> notify()); // OK
cf.thenRun(() -> log.info("Done"))
.thenAccept(v -> { }); // v — Void, useless
Architectural Trade-offs
| Method | Receives result | Returns | Use case |
|---|---|---|---|
| thenApply | ✅ T | U | Transformation |
| thenAccept | ✅ T | Void | Consumption |
| thenRun | ❌ | Void | Side effect |
Edge Cases
1. Exception handling:
// Exception in thenAccept:
cf.thenAccept(result -> {
throw new RuntimeException("Error processing");
}).exceptionally(ex -> {
// Will catch the exception
return null;
});
// thenRun also catches exceptions
cf.thenRun(() -> {
throw new RuntimeException("Error");
}).exceptionally(ex -> {
// Will catch
return null;
});
2. Async versions:
// thenAcceptAsync — executes in ForkJoinPool
cf.thenAcceptAsync(result -> process(result));
// With custom Executor
cf.thenAcceptAsync(result -> process(result), executor);
// thenRunAsync — same approach
cf.thenRunAsync(() -> notify(), executor);
Performance
thenApply: ~5 ns (function)
thenAccept: ~5 ns (consumer)
thenRun: ~3 ns (runnable — no access to T)
thenAcceptAsync / thenRunAsync:
- + ~1μs per thread switch
Production Experience
Logging and metrics:
@Service
public class OrderService {
public CompletableFuture<Order> createOrder(OrderRequest req) {
return orderRepository.saveAsync(req)
.thenApply(order -> {
metrics.increment("orders.created");
eventPublisher.publish(new OrderCreatedEvent(order));
return order; // thenApply returns result — order is in scope
});
}
// thenRun for side effects
public CompletableFuture<Void> sendNotification(Order order) {
return notificationClient.sendAsync(order)
.thenRun(() -> log.info("Notification sent for order {}", order.id()));
}
}
Best Practices
// ✅ thenAccept for consuming the result
cf.thenAccept(result -> process(result));
// ✅ thenRun for side effects
cf.thenRun(() -> metrics.increment("done"));
// ✅ Async versions with Executor
cf.thenAcceptAsync(result -> process(result), executor);
// ❌ thenRun when you need the result
// ❌ thenAccept when the result doesn't matter
// ❌ Blocking operations in thenAccept/thenRun
When NOT to use thenRun
When the result of the previous stage is needed for further processing. thenRun = “do something after, but I don’t care about the result”.
// ❌ thenRun — order is not accessible
orderRepository.saveAsync(req)
.thenRun(() -> notifyUser(order.id())); // compilation error: order not found
// ✅ thenAccept — order is accessible
orderRepository.saveAsync(req)
.thenAccept(order -> notifyUser(order.id()));
🎯 Interview Cheat Sheet
Must know:
- thenAccept(Consumer
) — receives result T, returns CompletableFuture - thenRun(Runnable) — does NOT receive the result, returns CompletableFuture
- thenAccept for consuming results, thenRun for side effects (logging, metrics)
- Both have Async versions with thread switching
- thenApply vs thenAccept: thenApply returns U, thenAccept returns Void
Frequent follow-up questions:
- When thenAccept, when thenRun? — thenAccept when you need the previous CF’s result, thenRun when the result doesn’t matter
- Can you continue the chain after thenAccept? — Yes, but the next link receives Void
- Does thenAccept block the thread? — No, it executes as a callback when the CF completes
- thenAccept vs thenApply — when which? — thenApply for transformation (returns U), thenAccept for consumption (returns Void)
Red flags (DO NOT say):
- “thenRun receives the previous CF’s result” — thenRun uses Runnable, no parameters
- “thenAccept permanently terminates the chain” — you can continue, but with Void
- “thenAcceptAsync is always better than thenAccept” — Async adds ~1μs overhead, excessive for lightweight operations
Related topics:
- [[4. What is the difference between thenApply() and thenCompose()]]
- [[6. How to handle exceptions in CompletableFuture chain]]
- [[11. What is the difference between thenApply() and thenApplyAsync()]]
- [[14. What is blocking code and how to distinguish it from non-blocking]]