Build a Promise from Scratch
advancedImplement a spec-faithful Promise with then chaining, state transitions, and async resolution.
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).Implement a spec-faithful Promise with then chaining, state transitions, and async resolution.
Implement Promise.all: aggregate results in order, fail fast on the first rejection.
Implement allSettled: wait for every promise and report per-promise status objects.
Implement Promise.any with AggregateError semantics: first success wins, all failures reject.