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.
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.
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.
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.
This pattern is easier to test because the transformation logic stays separate from logging or I/O.
Related Method You Should Not Confuse with Either
If the next step itself returns another CompletableFuture, you often want thenCompose, not thenApply. Otherwise you end up with a nested future.
That is a different distinction, but it often appears in the same code review conversations.
Common Pitfalls
- Using
thenAcceptand then wondering why the next stage no longer has a value to transform. - Using
thenApplyfor side effects only, which makes the chain look like it returns a meaningful value when it really does not. - Forgetting that
thenAcceptreturnsCompletableFuture<Void>, not the original result type. - Mixing transformation logic and side effects in every stage, which makes async flows harder to test.
- Using
thenApplywhen the lambda already returns aCompletableFuture, creating an unwanted nested future.
Summary
- '
thenApplytransforms a result and keeps the value flowing through the chain.' - '
thenAcceptconsumes a result for a side effect and ends that branch withCompletableFuture<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
thenComposeinstead of either method.

