What does thenApply do on a CompletableFuture, and how does it differ from thenAccept and thenRun?
answer
- Apply = Function (map), Accept = Consumer (side effect), Run = Runnable (ignore result)
- Apply returns CF<R>; Accept/Run return CF<Void>
- Runs only on SUCCESS; exception skips them
- non-Async runs on completing thread; Async uses ForkJoinPool/executor
basics
~10 sthenApply transforms the result into a new value (map). thenAccept consumes the result but returns nothing. thenRun runs an action that ignores the result entirely. All three run after the stage completes.
solid answer
~40 sAll three register a callback that runs once the upstream CompletableFuture completes successfully. They differ by what the callback receives and returns. thenApply takes a Function<T,R>: it receives the result and returns a transformed value, producing a CompletableFuture<R> — it maps a value. thenAccept takes a Consumer<T>: it receives the result, does a side effect, and returns CompletableFuture<Void>. thenRun takes a Runnable: it ignores the result completely and returns CompletableFuture<Void> — useful for fire-and-forget completion actions like logging or cleanup. By default the callback runs on the thread that completed the stage (or the caller's thread if already complete); the thenXxxAsync variants run it on the common ForkJoinPool or a supplied executor. If the upstream completed exceptionally, none of them run — the exception propagates downstream.
go deeper
Knows thenApply maps the value, thenAccept consumes it, thenRun ignores it, and can match each to Function/Consumer/Runnable.
Adds the return types (CF<R> vs CF<Void>) and that they fire only on success, with the exception skipping them.
Explains threading (completing thread vs Async/ForkJoinPool) and why the three functional interfaces exist; picks the right one by intent.
Discusses executor selection, thread-confinement and back-pressure implications of where callbacks run, and sets team conventions (always-Async with an explicit pool) for predictability.
## Background: what a CompletableFuture is A **CompletableFuture<T>** (from `java.util.concurrent`) represents a value of type `T` that may not exist yet — it is a *promise* of a future result. A *stage* is one CompletableFuture in a pipeline. When the computation finishes, the future is **completed**, either *normally* (with a value) or *exceptionally* (with a Throwable). You don't block and ask 'is it done?'; instead you **register callbacks** that the runtime invokes automatically when completion happens. This is the foundation of asynchronous, non-blocking code. ## The three single-dependency transforms These methods all attach exactly one callback to one upstream stage. They differ only in the **functional interface** the callback uses, which controls what it sees and what the new stage holds: | Method | Callback type | Receives result? | Returns a value? | New stage type | |---|---|---|---|---| | `thenApply` | `Function<T,R>` | yes | yes (`R`) | `CompletableFuture<R>` | | `thenAccept` | `Consumer<T>` | yes | no | `CompletableFuture<Void>` | | `thenRun` | `Runnable` | no | no | `CompletableFuture<Void>` | - **thenApply** is a **map**: 'when the value arrives, transform it into another value.' Example: a `CompletableFuture<String>` holding a JSON body → `thenApply(this::parse)` → `CompletableFuture<User>`. - **thenAccept** is a **terminal consumer with a result**: 'when the value arrives, do something with it' (write to DB, send to socket). It returns `CompletableFuture<Void>` so you can still chain *completion* actions, but downstream stages can't see the value. - **thenRun** **ignores the value**: 'when the previous step finishes, run this.' Its callback takes no arguments — good for cleanup, releasing a latch, or logging 'done'. ## When does the callback run, and on which thread? - It runs **only after successful completion** of the upstream stage. If the upstream finished *exceptionally*, the callback is **skipped** and the exception flows straight downstream (you'd handle it with `exceptionally`/`handle`). - **Thread:** the non-`Async` versions run the callback on whatever thread *completed* the upstream stage. If the stage was *already* complete when you call `thenApply`, it may run **synchronously on your calling thread**. The `thenApplyAsync` / `thenAcceptAsync` / `thenRunAsync` variants run it on the **common ForkJoinPool** (or an `Executor` you pass), which matters for blocking work or thread-confined resources. ## Why three methods instead of one Java's type system has no single 'callback' type — it has specialized functional interfaces. `Function` returns a value, `Consumer` does not, `Runnable` takes none. The three names map cleanly onto those, giving you precise, self-documenting intent and the correct return type without casting or `null` returns.
- If the upstream stage completes exceptionally, does thenApply's function run?No. thenApply (and thenAccept/thenRun) only fire on normal completion. On exceptional completion the function is skipped and the exception propagates to the next stage, where exceptionally/handle/whenComplete can deal with it.
- Which thread runs the thenApply callback?By default the thread that completed the upstream stage, or your calling thread if the stage is already complete. Use thenApplyAsync to force it onto the common ForkJoinPool or a supplied Executor.
saying these in an interview costs you the question
- Saying thenAccept or thenRun returns the original value — both return CompletableFuture<Void>.
- Thinking thenRun receives the result — it takes a Runnable and gets nothing.
- Assuming the callback always runs on a background thread — non-Async variants may run on the caller's thread.
- Believing thenApply still runs after an upstream exception.