What are the exact semantics of cancel() on a CompletableFuture, and what do obtrudeValue/obtrudeException do that you should rarely need?
answer
- cancel = completeExceptionally(CancellationException); flag ignored
- CF has no thread to interrupt — it's a result cell
- cancel propagates downstream, NOT upstream
- obtrudeValue/obtrudeException OVERWRITE even if settled
- obtrude = recovery/testing only; breaks write-once invariant
- stop real work at the executor/task layer, not on the future
basics
~20 scancel() on a CompletableFuture doesn't interrupt any thread — it just completes the future with a CancellationException; the mayInterruptIfRunning flag is ignored. obtrudeValue/obtrudeException forcibly overwrite an already-settled result, breaking the normal write-once rule — only for tests/recovery.
solid answer
~40 s`cancel(mayInterruptIfRunning)` on a `CompletableFuture` simply completes it **exceptionally with a CancellationException** if it wasn't already done; crucially the `mayInterruptIfRunning` flag is **ignored** because a plain CompletableFuture has no thread to interrupt — it's just a result holder, not bound to a running task. So 'cancel' here means 'reject this future's result', not 'stop the work'; a `supplyAsync` computation keeps running. Cancelling propagates downstream (dependent stages fail with CancellationException) but **not upstream** to the source. `obtrudeValue(v)` and `obtrudeException(ex)` are escape hatches that **forcibly overwrite** the result even if the future is already completed, violating the normal single-assignment/first-wins guarantee. They exist for error recovery and testing; in production they're a code smell because they break the invariant other code relies on and create races no one expects.
go deeper
Knows cancel() makes the future end with a CancellationException; not expected to know obtrude or interruption nuances.
Knows cancel completes the future exceptionally and that the running work isn't actually stopped; aware obtrude exists but rarely used.
Explains the ignored interrupt flag, downstream-only propagation, and that obtrude* overwrites a settled result for recovery/testing; routes real cancellation to the executor layer.
Frames the future as a single-assignment cell decoupled from its task, designs cancellation/cleanup at the task/executor boundary, fences off obtrude* as test/recovery-only, and reasons about the broken-invariant consequences for composition and resource leaks.
### Background: a CompletableFuture is a result holder, not a task A key mental model: a `CompletableFuture` is fundamentally a **single-assignment cell** for a result — it is **not** intrinsically tied to a thread or a running computation. Even `supplyAsync(task)` returns a future that is conceptually "a slot the task will fill"; the future doesn't *own* the task's thread. This explains every surprising thing below. ### cancel(mayInterruptIfRunning) ```java boolean cancel(boolean mayInterruptIfRunning); ``` From the `Future` interface, but CompletableFuture's implementation is special: - If the future is **already completed**, returns `false`, no effect. - If **incomplete**, it completes the future **exceptionally with a `CancellationException`** and returns `true`. After this `isCancelled()` and `isCompletedExceptionally()` are both `true`. - The **`mayInterruptIfRunning` flag has no effect** — the Javadoc states it because a CompletableFuture is not associated with a thread it could interrupt. So you cannot use `cancel(true)` to interrupt the `supplyAsync` worker. **Consequence:** "cancel" really means "settle this future as cancelled". The underlying computation (if any) is **not** stopped — it runs to completion, and its result is discarded because the future is already settled (first-wins). ### Propagation: downstream yes, upstream no Cancelling a future fails its **dependents** with a `CancellationException` (a `thenApply` after it won't run). But it does **not** propagate **upstream**: cancelling `b` in `a.thenApply(f) == b` does not cancel `a`. Each stage is its own cell. This asymmetry trips people up who expect cancellation to unwind the whole chain. ### So how do you actually stop work? You need a handle on the real task: submit a `Runnable`/`Callable` to an `ExecutorService` and cancel **that** `Future` with interruption, or pass a cancellation token your task polls, or use structured concurrency. The CompletableFuture's `cancel` alone only abandons the result. ### obtrudeValue and obtrudeException — the invariant-breakers ```java void obtrudeValue(T value); void obtrudeException(Throwable ex); ``` Normally a future is **write-once**: the first `complete`/`completeExceptionally`/`cancel` wins and the result is immutable thereafter. `obtrudeValue`/`obtrudeException` **bypass that rule** — they **forcibly overwrite** the result **even if the future is already completed**, and reset its state to the new value/exception. Unlike `complete`, they return `void` (no first-wins check) and always take effect. Why do they exist? The Javadoc says explicitly: for **error recovery** and **testing** — situations where you knowingly want to reset a stuck/wrong result. Why you should almost never use them in production: - They **break the single-assignment invariant** that all composition logic assumes. Dependent stages may have **already** fired on the old result; obtruding doesn't un-fire them, so you get an inconsistent view. - They introduce **races with no winner-determinism** — there's no boolean to tell you whether you clobbered a value. - They make reasoning about the future impossible: "settled" no longer means "final." The pragmatic principal-level stance: treat `obtrude*` as a **test-only / last-resort recovery** tool, fence it off, and never expose a future you obtrude on to general composition. ### Worked example ```java CompletableFuture<Integer> cf = CompletableFuture.supplyAsync(() -> slowCompute()); cf.cancel(true); // completes cf with CancellationException; slowCompute keeps running cf.isCancelled(); // true // cf.get() now throws CancellationException; slowCompute's eventual result is dropped CompletableFuture<String> g = new CompletableFuture<>(); g.complete("first"); g.obtrudeValue("second"); // forcibly overwrites — g.join() now returns "second" (invariant broken) ``` ### Putting it together for a senior/principal answer The through-line: a CompletableFuture is a **promise/result cell**, decoupled from the work that fills it. `cancel` settles the cell (no interruption); timeouts settle the cell (no interruption); only an `obtrude*` call can rewrite an already-settled cell — and doing so breaks the guarantees everyone else builds on. Real cancellation of work lives at the task/executor layer, not on the future.
- Why does mayInterruptIfRunning have no effect on a CompletableFuture?A CompletableFuture is a result holder not bound to any specific thread, so there is no running task it can interrupt. To interrupt actual work, cancel the ExecutorService Future that owns the thread instead.
- When are obtrudeValue/obtrudeException ever justified?Per the Javadoc, only for error recovery or testing — forcibly resetting a stuck or wrong result. They break the write-once invariant, so they must never be exposed to normal composition logic in production.
- Does cancelling a downstream stage cancel its upstream source?No. Cancellation propagates to dependents but not to the source. Each stage is an independent cell, so the upstream future and its work continue.
saying these in an interview costs you the question
- Believing cancel(true) interrupts the supplyAsync worker thread
- Expecting cancel to propagate upstream and stop the source
- Using obtrudeValue in production composition as if it were safe
- Assuming a 'settled' future is always final (obtrude can rewrite it)