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
routeChangeControlleraborts once, onpagehide, and its signal is shared by every call tosearch()— it represents "the page is going away," not "this one request timed out." - Each call to
search()gets its ownrequestController, so a fresh keystroke can cancel just its own in-flight request via the returnedcancel()function, independent of any other call. AbortSignal.timeout(8_000)produces a signal that aborts itself after 8 seconds — no manualsetTimeout+controller.abort()pairing required.AbortSignal.any([...])merges all three into the single signalfetchactually receives. Whichever of the three fires first decides why the request stops, andfetch's rejected promise carries that original reason inerror.name("AbortError"for a manual abort,"TimeoutError"for the timeout).
Gotchas
- The signal returned by
AbortSignal.any()is a real, independentAbortSignal— it does not expose which input signal caused the abort. Inspectsignal.reason(or catch the error and checkerror.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 intoany().Promise.any()— same "first one wins" shape, but for settling promises rather than merging cancellation signals.