Question 5 · Section 19

What do thenAccept() and thenRun() methods do

Both methods are used for completing a chain of CompletableFuture:

Language versions: English Russian Ukrainian

🟢 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 it
  • thenRun — 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

  1. 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]]