Auto-Retry for Promises
intermediateRetry a failing async operation N times with backoff — a small function with big production implications.
Use AbortController and AbortSignal to cancel fetch requests, add timeouts with AbortSignal.timeout, combine signals, and write abortable async utilities.
Promises have no built-in cancel — once started, a promise settles or hangs. But real applications must cancel constantly: the user typed another search character, navigated away, or a request exceeded its deadline. AbortController is the standard answer, and interviewers use it to separate candidates who've shipped production async code from those who've only done exercises.
const controller = new AbortController();
fetch("/api/search?q=hello", { signal: controller.signal })
.then((res) => res.json())
.catch((err) => {
if (err.name === "AbortError") return; // cancelled — usually not an error
throw err; // real failure
});
controller.abort(); // reject the fetch with AbortError, close the socketOne controller, one signal, many listeners. Key facts:
controller.abort(reason?) — aborts; signal.reason carries the reason (defaults to an AbortError DOMException).signal.aborted — synchronous check; signal.throwIfAborted() — throw if already aborted.AbortError from real failures in catch is the detail interviewers look for.The practical companion to debounce: debounce delays the call; abort kills the stale one that already left.
let controller = null;
async function search(query) {
controller?.abort(); // cancel the in-flight request
controller = new AbortController();
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
});
render(await res.json());
} catch (err) {
if (err.name !== "AbortError") showError(err);
}
}Without this, a slow response for "jav" can arrive after the response for "javascript" and clobber the UI — the classic race condition follow-up.
Modern platforms make the Promise.race timeout pattern one line:
// Abort automatically after 5 seconds
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
// Combine: whichever fires first wins (ES2024-era platform API)
const signal = AbortSignal.any([userController.signal, AbortSignal.timeout(5000)]);
const res2 = await fetch(url, { signal });AbortSignal.any is the composition primitive: user-initiated cancel or deadline, one signal. Knowing it exists usually ends the "how would you combine signals" follow-up immediately — and being able to sketch its polyfill (listen to each inner signal, abort the outer controller once) is bonus credit.
The interview exercise: "Write a sleep(ms, signal) that rejects when aborted."
function sleep(ms, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
return reject(signal.reason);
}
const id = setTimeout(() => {
signal?.removeEventListener("abort", onAbort);
resolve();
}, ms);
function onAbort() {
clearTimeout(id);
reject(signal.reason);
}
signal?.addEventListener("abort", onAbort, { once: true });
});
}The grading rubric hidden in this small function:
aborted first — the signal may already be dead.signal.reason, not a made-up error.The same shape makes anything abortable: wrap the operation, subscribe with { once: true }, and unsubscribe in the success path. Add a signal parameter to a promise pool or auto-retry and you've answered their hardest follow-ups.
async function fetchWithRetry(url, { retries = 3, signal } = {}) {
for (let attempt = 0; attempt <= retries; attempt++) {
signal?.throwIfAborted(); // stop between attempts
try {
return await fetch(url, { signal }); // stop mid-attempt
} catch (err) {
if (err.name === "AbortError" || attempt === retries) throw err;
await sleep(2 ** attempt * 100, signal); // abortable backoff
}
}
}AbortSignal is now the universal cancellation token: addEventListener(type, fn, { signal }) removes listeners in bulk when you abort (the modern cleanup pattern for components), and Node.js accepts signals in fs, stream, child_process, and setTimeout from timers/promises.
signal.aborted per iteration, or race the iterator against an abort promise (see iterator helpers).Retry a failing async operation N times with backoff — a small function with big production implications.
Run N async tasks with at most K in flight — the production-style p-limit pattern, including ordering and failure trade-offs.
Process a large list of async tasks in fixed-size sequential batches to protect downstream services.
Implement finally correctly: pass values through, and don't swallow rejections.