Promise Extensions and AbortSignal Statics
TL;DR
Promise got new static methods over the past few years: Promise.allSettled (waits for all, never rejects), Promise.any (first to fulfill wins), Promise.withResolvers (clean manual Promise construction). AbortSignal got AbortSignal.timeout(ms) and AbortSignal.any([signals]) for cancellation composition. These small additions replace common manual patterns (deferred objects, racing for “first success,” composing aborts).
Interview Q&A
Q: Promise.all vs allSettled vs any vs race — quick comparison.
A:
| Resolves with | Rejects when | Use for | |
|---|---|---|---|
Promise.all |
array of all results | any rejects (short-circuits) | “all must succeed” |
Promise.allSettled |
array of {status, value/reason} |
never | “do them all, report each outcome” |
Promise.any |
first fulfillment | all reject (with AggregateError) |
“first success wins; failures don’t kill it” |
Promise.race |
first to settle (fulfill or reject) | first rejection if it’s first | “whatever happens first” |
// allSettled — common for "fetch many things, render what worked"
const results = await Promise.allSettled([
fetch("/api/a"),
fetch("/api/b"),
fetch("/api/c"),
]);
const successful = results.filter(r => r.status === "fulfilled").map(r => r.value);
// any — first mirror to respond wins
const fastest = await Promise.any([
fetch("https://cdn1.example.com/file"),
fetch("https://cdn2.example.com/file"),
fetch("https://cdn3.example.com/file"),
]);
// race — timeouts (often replaced by AbortSignal.timeout now)
const winner = await Promise.race([
fetch("/api/slow"),
new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 5000)),
]);
Q: When use allSettled over all?
A: When partial failure is acceptable and you need to know which succeeded:
- Loading a dashboard’s multiple widgets — render the ones that loaded.
- Bulk operations where each item is independent.
- Status checks across multiple services.
const responses = await Promise.allSettled(urls.map(u => fetch(u)));
const failures = responses.filter(r => r.status === "rejected");
if (failures.length) reportPartialFailure(failures);
Use all only when every promise must succeed for the operation to proceed.
Q: Promise.any — when?
A: Racing redundant attempts where any success counts:
- Multi-region failover — try 3 region URLs in parallel; first one to return is your answer.
- Cached + network — try cache lookup and network in parallel; first response wins.
- Geolocation — try GPS, IP, last-known in parallel.
The reject path is AggregateError:
try {
const x = await Promise.any(promises);
} catch (err) {
if (err instanceof AggregateError) {
console.log("all failed:", err.errors); // array of individual errors
}
}
Q: Promise.withResolvers (ES2024) — what’s it replace?
A: The deferred pattern: create a Promise and access its resolve/reject from outside.
// Old — verbose
function deferred<T>() {
let resolve!: (v: T) => void, reject!: (e: unknown) => void;
const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej; });
return { promise, resolve, reject };
}
// ES2024
const { promise, resolve, reject } = Promise.withResolvers<string>();
setTimeout(() => resolve("done"), 1000);
await promise;
Useful for:
- Bridging callback APIs to async/await.
- “Wait for an external event” patterns.
- Event-based futures (next message, next click).
// Wait for the next WebSocket message
function nextMessage(socket: WebSocket): Promise<string> {
const { promise, resolve } = Promise.withResolvers<string>();
socket.addEventListener("message", (e) => resolve(e.data), { once: true });
return promise;
}
Q: AbortSignal.timeout(ms) — what’s it for?
A: A signal that aborts after a timeout — saves the manual setTimeout + controller.abort() plumbing:
// Before
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(url, { signal: controller.signal });
} finally { clearTimeout(timer); }
// After (ES2022)
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
Cleaner. Don’t forget to handle AbortError:
try {
await fetch(url, { signal: AbortSignal.timeout(5000) });
} catch (e) {
if (e instanceof DOMException && e.name === "TimeoutError") {
// it timed out
}
}
TimeoutError is the specific abort reason AbortSignal.timeout uses.
Q: AbortSignal.any([signals]) — composing aborts.
A: Combines multiple signals into one that fires when any of them does:
const userCancel = new AbortController();
const res = await fetch(url, {
signal: AbortSignal.any([
userCancel.signal, // user clicked cancel
AbortSignal.timeout(10_000), // overall timeout
routeChangeController.signal, // user navigated away
]),
});
// Any of the three aborting cancels the fetch.
Eliminates the need for the “compose multiple aborts” pattern most fetch wrappers had to roll by hand.
Q: How does AbortSignal work with non-fetch APIs?
A: Many modern APIs accept { signal }:
// addEventListener — auto-removes when signal aborts
const controller = new AbortController();
window.addEventListener("scroll", handler, { signal: controller.signal });
// Later: controller.abort() — handler is removed automatically. No manual removeEventListener.
// setTimeout (Node only — not browser yet)
const timer = setTimeout(callback, 1000); // no signal in browser setTimeout
// Streams API
stream.pipeTo(dest, { signal: controller.signal });
// ReadableStream consumer
const reader = stream.getReader({ signal });
The signal option on addEventListener is underused — removes the need to track listener references for cleanup.
Q: Promise and unhandled rejections.
A: A Promise that rejects without a .catch or await produces an unhandled rejection event:
// Browser
window.addEventListener("unhandledrejection", (e) => {
console.warn("unhandled:", e.reason);
e.preventDefault(); // suppress default warning
});
// Node
process.on("unhandledRejection", (reason) => {
console.error("unhandled rejection:", reason);
});
For monitoring (Sentry/Datadog), wire to the error tracker. Unhandled rejections in production usually signal a real bug — a missing await on a fire-and-forget that should have logged its failure.
Q: What about Promise.try (Stage 4 / ES2025)?
A: Wraps a function call (sync or async) in a Promise, catching sync throws too:
// Without — sync throws escape
Promise.resolve().then(() => syncFunctionThatMightThrow()); // ok
Promise.resolve(syncFunctionThatMightThrow()); // sync throw uncaught
// With Promise.try
const promise = Promise.try(syncFunctionThatMightThrow);
promise.catch(handleError); // catches both sync throws and async rejections
Useful when you don’t know if a function is sync or async, or want uniform error handling.
Gotchas / edge cases
Promise.racewith no fulfillment — hangs forever if no promise resolves. Pair withAbortSignal.timeoutor a backup promise that rejects after a deadline.Promise.allshort-circuits on rejection — the other promises still run (you can’t cancel them viaall). For cancel-on-first-error, you need explicit AbortController in each.- Unhandled rejection in
.thenchain —p.then(a).then(b)wherebrejects has no handler. Always end chains with.catch(orawait). AbortSignal.timeoutbrowser support — landed in 2022; safe in evergreens. Polyfill for older.AbortSignal.anybrowser support — newer (2024); polyfill viaabort-controller.AbortControllerreuse — once aborted, the controller’s signal stays aborted. Use a new controller per logical operation.- Race conditions in
withResolvers— callingresolveafterreject(or vice versa) does nothing. Same asnew Promise((res, rej) => ...)semantics.
What a senior is expected to say
- “
Promise.allfor must-all-succeed;allSettledfor must-do-all-report-each;anyfor first-success-wins;racefor first-settles. Pick by failure tolerance.” - “
Promise.withResolvers(ES2024) replaces the deferred pattern — cleanest way to bridge callback APIs to async/await.” - “
AbortSignal.timeout(ms)over manualsetTimeout + abortplumbing.AbortSignal.anycomposes multiple cancel sources (user, timeout, navigation).” - “
addEventListeneraccepts{ signal }— abort the controller and the listener auto-removes. Underused, removes the cleanup-tracking boilerplate.” - “Unhandled rejection handler wired to your error tracker — uncaught rejections in prod are real bugs.”
Cross-references
- AbortController + race conditions deep: ../11_apis_data_fetching/05_abort_and_race_conditions.md
- Retries / backoff: ../11_apis_data_fetching/06_retries_backoff_idempotency.md
- Async / Promises basics: ../03_javascript_core/async_js/
Further reading
- MDN — Promise: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
- MDN — AbortSignal: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
- TC39 — Promise.try: https://github.com/tc39/proposal-promise-try