10LOC
Web Platformintermediate

AbortSignal.any() to combine multiple abort signals into one

Published August 4, 2026

const routeChangeController = new AbortController();
window.addEventListener("pagehide", () => routeChangeController.abort(), { once: true });

export const search = (query: string) => {
  const requestController = new AbortController();
  const timeoutSignal = AbortSignal.timeout(8_000);
  const signal = AbortSignal.any([requestController.signal, routeChangeController.signal, timeoutSignal]);
  const result = fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal });
  return { result, cancel: () => requestController.abort() };
};

What

AbortSignal.any(signals) is a static method that takes an iterable of AbortSignals and returns one combined signal. The returned signal aborts as soon as any of the input signals aborts, carrying that signal's abort reason.

Why it matters

A single async operation often needs to respond to more than one cancellation source at once. A search request should stop if the user types again (cancel the stale request), if the page is navigating away, and if it's simply taking too long. Before AbortSignal.any(), combining those meant writing your own merger: create a new AbortController, attach an abort listener to every source signal, and remember to remove those listeners once one of them fires — easy to get wrong, easy to leak listeners on the signals you didn't end up needing.

AbortSignal.any() does exactly that internally, correctly, in one call. It's also lazy about listeners: if one input signal is already aborted when you call it, the returned signal is synchronously born aborted — no listener setup needed at all.

How it works

  • A module-level routeChangeController aborts once, on pagehide, and its signal is shared by every call to search() — it represents "the page is going away," not "this one request timed out."
  • Each call to search() gets its own requestController, so a fresh keystroke can cancel just its own in-flight request via the returned cancel() function, independent of any other call.
  • AbortSignal.timeout(8_000) produces a signal that aborts itself after 8 seconds — no manual setTimeout + controller.abort() pairing required.
  • AbortSignal.any([...]) merges all three into the single signal fetch actually receives. Whichever of the three fires first decides why the request stops, and fetch's rejected promise carries that original reason in error.name ("AbortError" for a manual abort, "TimeoutError" for the timeout).

Gotchas

  • The signal returned by AbortSignal.any() is a real, independent AbortSignal — it does not expose which input signal caused the abort. Inspect signal.reason (or catch the error and check error.name) if the caller needs to distinguish "timed out" from "user cancelled."
  • Passing an already-aborted signal into AbortSignal.any() returns an already-aborted signal immediately; it won't wait for a microtask, so don't rely on the call itself being async.

Related

  • AbortSignal.timeout() — a signal source in its own right, and often one of the inputs you feed into any().
  • Promise.any() — same "first one wins" shape, but for settling promises rather than merging cancellation signals.