Promise.all
intermediateImplement Promise.all: aggregate results in order, fail fast on the first rejection.
Implement a spec-faithful Promise in JavaScript: state machine, microtask timing, handler passthrough, and the thenable resolution procedure that makes chaining work.
Implement a
MyPromiseclass that passes for the real thing:new MyPromise(executor),then/catch/finally, chaining, and correct async ordering.
This is the definitive senior async question because it has three difficulty cliffs, and interviewers watch which ones you fall off:
then(() => fetch(...)) — the whole point of promises — doesn't work.There's a fourth, subtler cliff: handler passthrough. then called with non-function handlers must forward the value/reason. Get it wrong and p.then(fn).catch(handleErr) breaks — catch is literally then(undefined, fn), so a fulfilled promise flowing through catch hits your undefined "handler."
class MyPromise {
#state = "pending";
#result = undefined;
#handlers = []; // queued { onFulfilled, onRejected, resolve, reject }
constructor(executor) {
try {
executor(this.#resolve.bind(this), this.#reject.bind(this));
} catch (err) {
this.#reject(err); // a throwing executor rejects the promise
}
}
// ---- The Promise Resolution Procedure (the third cliff) ----
#resolve(value) {
if (this.#state !== "pending") return; // settle-once
if (value === this) {
return this.#reject(new TypeError("Chaining cycle detected"));
}
// If value is a thenable, ADOPT its eventual state instead of
// fulfilling with the thenable itself. This is what makes
// `then(() => anotherPromise)` wait.
if (value !== null && (typeof value === "object" || typeof value === "function")) {
let then;
try {
then = value.then; // getter may throw — spec says reject then
} catch (err) {
return this.#reject(err);
}
if (typeof then === "function") {
let called = false; // a hostile thenable may call back twice
try {
then.call(
value,
(v) => { if (!called) { called = true; this.#resolve(v); } }, // recurse: v may itself be thenable
(r) => { if (!called) { called = true; this.#reject(r); } }
);
} catch (err) {
if (!called) { called = true; this.#reject(err); }
}
return;
}
}
this.#state = "fulfilled";
this.#result = value;
this.#flush();
}
#reject(reason) {
if (this.#state !== "pending") return;
this.#state = "rejected";
this.#result = reason;
this.#flush();
}
#flush() {
this.#handlers.forEach((h) => this.#schedule(h));
this.#handlers = [];
}
// ---- Microtask timing + handler passthrough (cliffs 2 and 4) ----
#schedule({ onFulfilled, onRejected, resolve, reject }) {
queueMicrotask(() => {
try {
if (this.#state === "fulfilled") {
if (typeof onFulfilled === "function") {
resolve(onFulfilled(this.#result));
} else {
resolve(this.#result); // passthrough: then(null).then(v => ...) works
}
} else {
if (typeof onRejected === "function") {
resolve(onRejected(this.#result)); // handled rejection → chain RECOVERS
} else {
reject(this.#result); // passthrough: rejection tunnels to the next catch
}
}
} catch (err) {
reject(err); // a throwing handler rejects the derived promise
}
});
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const handler = { onFulfilled, onRejected, resolve, reject };
if (this.#state === "pending") {
this.#handlers.push(handler); // settle later → flush will schedule
} else {
this.#schedule(handler); // already settled → still a microtask, never sync
}
});
}
catch(onRejected) {
return this.then(undefined, onRejected);
}
finally(onFinally) {
return this.then(
(value) => MyPromise.resolve(onFinally()).then(() => value),
(reason) => MyPromise.resolve(onFinally()).then(() => { throw reason; })
);
}
static resolve(value) {
return value instanceof MyPromise
? value
: new MyPromise((resolve) => resolve(value));
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
}// 1. Microtask timing (cliff 2)
const p = MyPromise.resolve("A");
p.then(console.log);
console.log("B");
// B, then A — handlers NEVER run synchronously, even when already settled
// 2. catch on a FULFILLED promise (cliff 4 — the passthrough)
MyPromise.resolve("ok")
.then((v) => v + "!")
.catch(() => "never runs")
.then(console.log); // "ok!" — value tunnels through catch untouched
// 3. Returning a promise from then (cliff 3 — resolution procedure)
MyPromise.resolve(1)
.then((v) => new MyPromise((res) => setTimeout(() => res(v + 1), 10)))
.then(console.log); // 2 — the chain WAITED for the inner promise
// 4. Error recovery
MyPromise.reject(new Error("boom"))
.catch((e) => "recovered")
.then(console.log); // "recovered" — a handled rejection fulfills the chainIf your implementation passes these four, you've cleared every cliff. (The full Promises/A+ compliance suite has 872 tests; these four cover the categories that fail most.)
queueMicrotask — the spec requires handlers to run on a clean stack. Without it, then callbacks on settled promises run synchronously and reorder against the event loop — the "Zalgo" bug: an API that's sometimes sync, sometimes async, so callers can't reason about ordering.called flag — a malicious or buggy thenable can invoke both callbacks, or one callback twice. Settle-once must survive that. This is a real interoperability rule from Promises/A+ §2.3.3.3.3, not paranoia.resolve(onRejected(...)) not reject(...) — a rejection handler that returns normally recovers the chain. This asymmetry (fulfill on return, reject on throw) is the entire error-handling model.#resolve — a thenable can resolve to another thenable; adoption must chase the chain to a plain value.Promise.resolve("ok")
.catch(() => "rescued")
.then((v) => console.log(v));resolve called twice / after reject — first settlement wins, silently.p's resolve called with p → TypeError: Chaining cycle detected (yes, the spec names this).then called multiple times on one promise — every handler runs, in registration order. That's why #handlers is an array, not a single slot.finally passthrough — finally forwards the original value/reason, but if onFinally throws or returns a rejected promise, that error wins. Both behaviors fall out of the then-based implementation above.catch on a fulfilled promise crashes with undefined is not a function (fails snippet 2)..then(console.log) prints a promise object (fails snippet 3).catch(reject) wiring that can't recover — chains where one rejection poisons everything downstream even through handlers (fails snippet 4).static all/race/any with bugs (empty-array hangs are near-universal). The combinators are their own questions with their own edge cases — Promise.all, Promise.race, Promise.any, Promise.allSettled — and in an interview it's better to nail one than to sketch five.setTimeout(fn, 0)?" — ordering guarantees: all microtasks drain before the next macrotask, so promise chains complete before timers and rendering. Full story in the event loop.async/await relate?" — async functions are compiled into this machinery: each await is a then, the function body after it is the handler, and Promise.withResolvers exposes the raw resolve/reject the executor pattern hides.unhandledrejection; a userland implementation can approximate it with a microtask-deferred check. Knowing the event's name is usually enough.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.
Implement Promise.race and use it for the timeout pattern every senior interview touches.