Java
CompletableFuture
thenAccept
thenApply
asynchronous programming

Difference between thenAccept and thenApply

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

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.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.