Java
CompletableFuture
thenAccept
thenApply
asynchronous programming

Difference between thenAccept and thenApply

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

thenApply and thenAccept are both continuation methods on CompletableFuture, but they do different jobs. thenApply transforms a value and passes the transformed result forward, while thenAccept consumes a value for a side effect and ends that branch with CompletableFuture<Void>.

If you remember one rule, make it this: use thenApply when you still need a value later, and use thenAccept when you are done computing and only want to do something with the result.

thenApply Transforms the Result

thenApply takes a Function<T, R>. It receives the completed value and returns a new value.

java
1import java.util.concurrent.CompletableFuture;
2
3public class ThenApplyDemo {
4    public static void main(String[] args) {
5        CompletableFuture<String> future = CompletableFuture
6            .supplyAsync(() -> 21)
7            .thenApply(n -> n * 2)
8            .thenApply(n -> "result=" + n);
9
10        System.out.println(future.join());
11    }
12}

The final future still has a meaningful value, in this case a String. That makes thenApply appropriate for mapping, formatting, parsing, filtering, and building the next stage of a pipeline.

thenAccept Consumes the Result

thenAccept takes a Consumer<T>. It receives the completed value but does not return another one.

java
1import java.util.concurrent.CompletableFuture;
2
3public class ThenAcceptDemo {
4    public static void main(String[] args) {
5        CompletableFuture<Void> future = CompletableFuture
6            .supplyAsync(() -> "order-123")
7            .thenAccept(id -> System.out.println("completed order " + id));
8
9        future.join();
10    }
11}

The continuation is for a side effect such as logging, publishing, updating a UI, or writing a metric. Because no value continues down the chain, the return type becomes CompletableFuture<Void>.

Read the Return Type as Intent

The fastest way to choose between the two is to read the signature intent:

  • 'thenApply: “take a result and produce another result”'
  • 'thenAccept: “take a result and do something observable with it”'

That distinction matters when you chain multiple asynchronous steps.

java
1CompletableFuture<String> future = CompletableFuture
2    .supplyAsync(() -> "  hello  ")
3    .thenApply(String::trim)
4    .thenApply(String::toUpperCase);

If you replace the first thenApply with thenAccept, the later transformation cannot continue because the pipeline no longer carries a string value.

Side Effects Versus Data Flow

A useful design rule is to delay side effects until the end of the pipeline. Keep intermediate stages pure when possible.

java
1CompletableFuture<Void> future = CompletableFuture
2    .supplyAsync(() -> 10)
3    .thenApply(n -> n + 5)
4    .thenApply(n -> "final value: " + n)
5    .thenAccept(System.out::println);

This pattern is easier to test because the transformation logic stays separate from logging or I/O.

If the next step itself returns another CompletableFuture, you often want thenCompose, not thenApply. Otherwise you end up with a nested future.

java
CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> "42")
    .thenCompose(s -> CompletableFuture.supplyAsync(() -> "parsed=" + Integer.parseInt(s)));

That is a different distinction, but it often appears in the same code review conversations.

Common Pitfalls

  • Using thenAccept and then wondering why the next stage no longer has a value to transform.
  • Using thenApply for side effects only, which makes the chain look like it returns a meaningful value when it really does not.
  • Forgetting that thenAccept returns CompletableFuture<Void>, not the original result type.
  • Mixing transformation logic and side effects in every stage, which makes async flows harder to test.
  • Using thenApply when the lambda already returns a CompletableFuture, creating an unwanted nested future.

Summary

  • 'thenApply transforms a result and keeps the value flowing through the chain.'
  • 'thenAccept consumes a result for a side effect and ends that branch with CompletableFuture<Void>.'
  • Choose based on whether a later stage still needs a value.
  • Keep transformation stages pure where possible and push side effects to the edge.
  • If the next step returns another future, look at thenCompose instead of either method.

Course illustration
Course illustration

All Rights Reserved.