Compare the ShutdownOnFailure and ShutdownOnSuccess policies. When would you use each, and how do they change error and cancellation propagation?
answer
- OnFailure = need ALL, fail-fast, throwIfFailed()
- OnSuccess = need ONE, race, result()
- both: shutdown interrupts surviving siblings (cancellation down)
- outcome propagates up: exception vs winning value
- newer JDK: Joiner.allSuccessfulOrThrow / anySuccessfulResultOrThrow
basics
~20 sShutdownOnFailure (fan-out, need all): the moment any subtask fails, the scope cancels the rest and the failure is surfaced — use it when you need every result. ShutdownOnSuccess (race, need one): the first success cancels the rest — use it when any one winner is enough.
solid answer
~50 sBoth are completion policies that decide when join() can stop early and what happens to the other subtasks. ShutdownOnFailure is the all-or-nothing fan-out: you need every subtask's result, so as soon as any one fails it shuts the scope down — interrupting the still-running siblings — and lets you re-throw the first exception via throwIfFailed(). Use it when an operation aggregates several required results (fetch user AND orders AND prefs); one failure makes the whole thing pointless, so you fail fast and don't waste work. ShutdownOnSuccess is the race: you only need one winner, so the first successful subtask shuts the scope down, cancels the losers, and you read it via result(). Use it for redundant or hedged requests — query three mirrors, take whichever answers first. In both, shutdown interrupts the surviving subtasks (cancellation propagates down to children), and the controlling outcome propagates up to the parent. In newer JDKs these are expressed as Joiner.allSuccessfulOrThrow() and anySuccessfulResultOrThrow(), but the semantics are identical.
code
java · 16 lines// Need ALL results, fail fast:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var u = scope.fork(() -> fetchUser(id));
var o = scope.fork(() -> fetchOrders(id));
scope.join(); // returns early on first failure
scope.throwIfFailed(); // re-throw it; siblings already cancelled
return new Page(u.get(), o.get());
}
// Need ANY one result, race:
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
scope.fork(() -> queryMirror("eu"));
scope.fork(() -> queryMirror("us"));
scope.join(); // returns on first success; losers cancelled
return scope.result(); // the winning value
}go deeper
Can say OnFailure = need all and stops on a failure; OnSuccess = need one and stops on the first success.
Maps each policy to a use case (aggregate-all vs race) and knows throwIfFailed()/result() read the outcome.
Explains cancellation propagation downward, error/result propagation upward, the invokeAll/invokeAny analogy, and the plain no-policy variant.
Discusses choosing policies for resilience (hedging, fail-fast aggregation), interrupt cooperation in subtasks, tail-latency trade-offs, and the Joiner rename across JDK previews.
## Why policies exist When you fork several subtasks, what should happen if one fails, or if you only need one to succeed? A **completion policy** (called a `Joiner` in newer JDKs) answers that: it decides **when `join()` is allowed to return early** and what to do with the **other** subtasks. The two classic policies are `ShutdownOnFailure` and `ShutdownOnSuccess`. Key shared mechanic: **"shutdown"** means the scope stops accepting/awaiting more work and **interrupts every subtask that is still running**. Interruption is how **cancellation propagates downward** from the scope to its children. Whatever the policy decides also **propagates upward** to the parent (a re-thrown exception, or the winning result). ## `ShutdownOnFailure` — invokeAll / "I need them all" Pattern: a **fan-out** where you need **every** result and any single failure ruins the operation. ```java try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Subtask<User> u = scope.fork(() -> fetchUser(id)); Subtask<Order> o = scope.fork(() -> fetchOrders(id)); Subtask<Prefs> p = scope.fork(() -> fetchPrefs(id)); scope.join(); // wait for all, OR stop early on first failure scope.throwIfFailed(); // re-throw the first failure as the cause return new Page(u.get(), o.get(), p.get()); } ``` - If **all** succeed, `join()` waits for all, `throwIfFailed()` is a no-op, you read each result. - If **any** subtask fails, the scope **shuts down immediately** — the other in-flight subtasks are **interrupted/cancelled** (no point computing results you'll throw away), `join()` returns, and `throwIfFailed()` **re-throws** the first failure (wrapping it as the cause). This is **fail-fast**. - **Use when:** aggregating multiple *required* results — building a page from several services, scatter-gather where partial results are useless. ## `ShutdownOnSuccess` — race / "I need just one" Pattern: a **race** among redundant attempts where the **first success wins** and the rest are wasted effort. ```java try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) { scope.fork(() -> queryMirror("eu")); scope.fork(() -> queryMirror("us")); scope.fork(() -> queryMirror("ap")); scope.join(); // returns as soon as one succeeds return scope.result(); // the winning value } ``` - The **first subtask to succeed** triggers shutdown: `join()` returns, the **losing subtasks are interrupted/cancelled**, and `result()` gives the winner's value. - If **all** fail, `result()` throws (the last/aggregated failure). - **Use when:** hedged or redundant requests, latency reduction by querying several replicas and taking the fastest, fallback chains where any one source suffices. ## The propagation summary | | ShutdownOnFailure | ShutdownOnSuccess | |---|---|---| | Trigger to stop early | first **failure** | first **success** | | Fate of siblings | interrupted/cancelled | interrupted/cancelled | | Up-propagation | re-throw via `throwIfFailed()` | winning value via `result()` | | You need… | **all** results | **any one** result | | Mental model | `invokeAll` + fail-fast | `invokeAny` | **Cancellation propagation (downward):** shutdown interrupts surviving children; well-behaved subtasks observe the interrupt (during blocking calls or by checking `Thread.interrupted()`) and stop promptly. **Error/result propagation (upward):** the controlling outcome reaches the parent block. ## A neutral option There is also a plain scope with **no** early-stop policy: it forks, `join()` waits for **all** to finish (success or failure), and you inspect each subtask's `state()` yourself. Use it when you genuinely want every subtask to run to completion and handle results/failures individually (no fail-fast). ## JDK evolution In JDK 21/22 these were concrete subclasses `StructuredTaskScope.ShutdownOnFailure` / `ShutdownOnSuccess`. Later previews (JDK 24/25) replaced them with a **`Joiner`** abstraction passed at scope creation — e.g. `Joiner.allSuccessfulOrThrow()` (= shutdown-on-failure) and `Joiner.anySuccessfulResultOrThrow()` (= shutdown-on-success). **The names changed; the semantics — first-failure-cancels-all vs first-success-cancels-all — did not.** In an interview, lead with the semantics and mention the rename.
- When a ShutdownOnFailure scope shuts down, what happens to the subtasks still running?They are interrupted (cancellation propagates downward). Well-behaved subtasks observe the interrupt during blocking operations or by checking the interrupt flag and stop promptly, so no wasted work continues.
- Give a realistic use case for ShutdownOnSuccess.Hedged/redundant requests: query several replicas or mirrors of the same data in parallel and take whichever responds first, cancelling the slower ones to cut tail latency.
- Is there a scope that doesn't stop early at all?Yes — a plain scope with no shutdown policy: join() waits for every subtask to finish (success or failure) and you inspect each subtask's state individually.
saying these in an interview costs you the question
- Swapping them: thinking ShutdownOnSuccess stops on failure or vice versa.
- Claiming the losing/sibling subtasks keep running after shutdown — they're interrupted/cancelled.
- Saying ShutdownOnFailure returns partial results — it re-throws; you don't use the partial set.
- Believing shutdown forcibly kills threads — it interrupts; subtasks must cooperate to stop promptly.
- Not knowing throwIfFailed()/result() are how the outcome reaches the parent.