skip to content

What does AbortSignal.any() do in JavaScript, and why would you use it instead of just forwarding the signal you were handed?

level: seniorimportance: nice to knowfreq 26%

answer

  1. OR over cancellation sources
  2. first source wins, reason included
  3. already-aborted input latches it immediately
  4. no controller, purely derived
  5. build it per operation, not per module

basics

~20 s

AbortSignal.any(signals) returns a composite signal that aborts as soon as any input signal aborts, adopting that signal's reason. Use it when an operation must honour a caller's signal plus another condition — an internal deadline or a shutdown signal — without touching the caller's controller.

solid answer

~60 s

`AbortSignal.any(iterable)` builds a *dependent* signal from several sources. It aborts the instant the first source aborts, and its `reason` is that source's reason — so a caller cancellation still surfaces as `AbortError` while an expired `AbortSignal.timeout(ms)` surfaces as `TimeoutError`, and you can tell them apart downstream. If any input is already aborted at construction time, the composite comes back already aborted. You need it whenever your layer wants to add a cancellation condition to one it was given: you must not call `abort()` on the caller's controller (you do not own it, and you would cancel work you know nothing about), and forwarding only your own signal would drop their cancellation. The composite has no controller of its own — nothing can abort it directly, only a source can — and the relationship is one-way: aborting the composite is not even possible, so sources are never affected. Before it existed you wired this by hand with a fresh controller and forwarded `abort` listeners, which is the version that leaks if you forget to unsubscribe.

code

javascript · 13 lines
javascript
function withDeadline(callerSignal, ms) {
  // aborts on caller cancellation OR on our own deadline, whichever comes first
  return AbortSignal.any([callerSignal, AbortSignal.timeout(ms)]);
}

const caller = new AbortController();
const signal = withDeadline(caller.signal, 5000);

signal.addEventListener('abort', () => {
  console.log(signal.reason.name); // "AbortError" or "TimeoutError" — cause preserved
}, { once: true });

caller.abort(); // caller wins the race: reason.name === "AbortError"

go deeper

for a junior

Know that several cancellation sources can be combined into one signal, and that the combined signal aborts when the first of them does.

for a middle

Explain the semantics precisely: first abort wins, the reason is adopted verbatim, an already-aborted input makes the composite already aborted, and the result has no controller of its own.

for a senior

Show why forwarding a caller's signal untouched while adding your own condition requires composition rather than a new controller, and pair it with AbortSignal.timeout so a deadline and a caller cancellation stay distinguishable at the catch site.

for a principal

Own the cross-cutting policy: where deadlines are injected into a call chain, how cancellation causes are classified in telemetry, and how you keep composition per-operation so a shared latched signal never poisons unrelated work.

## The situation it exists for You are handed a `signal` by your caller, and your own layer has an additional reason to stop — a deadline, a shutdown flag, a newer request superseding this one. Three tempting moves are all wrong: - **Call `abort()` on the caller's controller.** You do not have it (you were given a signal, not a controller), and even if you did, aborting it would cancel every other operation sharing it. - **Ignore the caller's signal and use your own.** Their cancellation is now silently dropped, which is the bug that leaves requests running after the user navigated away. - **Pick whichever seems more important.** Cancellation is not a priority question; *any* reason to stop is sufficient. What you need is an OR over signals, and that is exactly `AbortSignal.any`. ## Semantics ```js function withDeadline(work, callerSignal, ms) { const signal = AbortSignal.any([callerSignal, AbortSignal.timeout(ms)]); return work({ signal }); } ``` - It takes an **iterable** of signals and returns a new `AbortSignal`. - The composite aborts on the **first** source abort, and takes that source's `reason` verbatim. Later aborts from other sources change nothing, because the composite is already latched. - If **any input is already aborted** when you call it, the returned signal is already aborted with that reason — the pre-check inside your abortable function then rejects immediately, exactly as intended. - The composite has **no controller**. There is no way to abort it directly; it is purely derived. That is a feature: you can hand it to a callee knowing that nothing downstream can cancel your sources. - Propagation is **one-way, downstream only**. A source abort reaches the composite; nothing reaches back up. Composites can be composed further, since a composite is itself a signal. ## Why the reason matters Because the composite adopts the source reason rather than inventing one, the distinction survives: ```js try { await withDeadline(doWork, callerSignal, 5000); } catch (err) { if (err.name === 'TimeoutError') { /* our deadline expired */ } else if (err.name === 'AbortError') { /* the caller cancelled */ } else throw err; } ``` That is the practical payoff of pairing `AbortSignal.any` with `AbortSignal.timeout`: two different causes, two different names, one signal threaded through the call. ## The manual version, and what it costs Before `AbortSignal.any`, the same composition was written by hand: ```js function anyOf(signals) { const controller = new AbortController(); const onAbort = (e) => controller.abort(e.target.reason); for (const s of signals) { if (s.aborted) { controller.abort(s.reason); break; } s.addEventListener('abort', onAbort, { once: true }); } return controller.signal; } ``` This is correct only if you also **unsubscribe from every source** once the composite settles or the operation finishes. Forget that, and a long-lived source signal accumulates one listener per composed operation. The built-in exists partly because that cleanup was so routinely omitted; it manages the linkage itself, and the specification is written so a dependent signal nobody references can be collected along with its linkage. ## Scoping Build the composite **per operation**, not once per module. A composite created from a long-lived source and then reused couples unrelated work: whichever source aborts first latches it permanently, and every later operation using it starts already-aborted. Create it where the operation starts, pass it down, and let it go out of scope when the operation finishes. ## Availability `AbortSignal.any` is newer than the rest of the abort API — it landed in browsers and Node meaningfully later than `AbortSignal.timeout` and `signal.reason`. On an older baseline, the manual `anyOf` above is the fallback, and the mention of it in an interview is a fair signal that you have shipped this rather than only read about it.

  • Why not just call abort() on the signal you were given when your own deadline expires?
    You cannot — a signal has no `abort()` method; only its controller does, and you were deliberately not given it. Even with access, aborting the caller's controller would cancel every other operation sharing that signal, including work you know nothing about. Composition adds your condition without reaching into someone else's lifetime.
  • What is the composite's reason when both a caller abort and a timeout fire?
    Whichever aborted first. The composite latches on the first source abort and copies that source's reason verbatim; later aborts are ignored because an aborted signal never changes. That is what lets a caller distinguish `AbortError` from `TimeoutError` downstream and decide whether to retry or stay quiet.
  • Can anything abort the signal returned by AbortSignal.any directly?
    No. It is a derived signal with no controller, so it aborts only when one of its sources does. That makes it safe to hand to a callee: nothing downstream can reach back and cancel your sources. If you need a trigger of your own, add a fresh controller's signal to the input list.
  • What breaks if you create one composite signal at module scope and reuse it?
    It latches permanently the first time any source aborts, so every subsequent operation using it starts already-aborted and fails immediately. It also couples unrelated work to a single cancellation. Composites are per-operation values: build one where the work starts and let it go out of scope when the work ends.

saying these in an interview costs you the question

  • Calls abort on the caller's controller to add a deadline
  • Thinks the composite can be aborted directly without a source
  • Believes aborting the composite propagates back to the sources
  • Reuses one composite signal across many operations
  • Hand-rolls composition and never unsubscribes from the sources

context