JavaScript Polyfill Interview Questions: Promise.all and Bind
How to implement Promise.all, bind, new, reduce, a Promise class and JSON.stringify for JavaScript interviews, and the spec detail each answer hinges on.
- Course: JavaScript study plan
- Module: Interview idioms
- Kind: Lesson
- Reading time: 14 min
- Runtime: Node 16
How do you implement Promise.all in JavaScript?
Return a new promise that wraps each input with Promise.resolve, attaches a then handler, and stores each value at its own index, so the results keep input order rather than completion order. A counter of pending items resolves the outer promise when it reaches zero, the first rejection rejects it, and an empty input resolves [] immediately.
Lesson
"Implement Promise.all." "Write bind." "How would you implement Array.prototype.reduce?" These questions test whether you understand the semantics of things you use every day — not whether you can recite them. The trap is that the obvious ten-line version is wrong in ways the interviewer knows about: bind must work with new, map must skip holes, reduce must throw on an empty array without a seed, Promise.all must preserve order and reject on the first failure, JSON.stringify must drop undefined in objects but write null in arrays. This lesson lists the questions that recur, the spec details each one hinges on, and the shape of a correct answer; the exercises and the checkpoint have you write them.
The recurring list
| Ask | The detail that decides it | |
|---|---|---|
call/apply/bind | Set this via a temporary symbol-keyed property (or Reflect.apply); bind pre-applies arguments and is ignored as this when the bound function is called with new — detect via this instanceof bound or new.target | |
new | Create an object whose prototype is Ctor.prototype, call Ctor with it as this, return the object unless the constructor returned an object | |
instanceof | Walk Object.getPrototypeOf from the value until null, comparing to Ctor.prototype; primitives are false; honour Symbol.hasInstance if asked | |
Object.create | An object with the given prototype (null allowed) plus optional descriptors | |
Array.prototype.map/filter/forEach | Iterate 0…length−1, skip holes (i in arr), pass (value, index, array), honour thisArg; map preserves length and holes | |
reduce | With no initial value start from the first present element; throw TypeError on an empty array without a seed | |
flat(depth) | Recursive with depth − 1, Infinity allowed, holes removed, depth default 1; flatMap is map then flat(1) | |
includes versus indexOf | includes uses SameValueZero (finds NaN); indexOf uses === (never finds NaN) | |
Promise.all | Result array in input order, resolve when the counter hits length, reject on the first rejection, non-promise inputs are wrapped (Promise.resolve), an empty input resolves [] immediately | |
Promise.allSettled / race / any | allSettled never rejects — `{status, value | reason}; race settles with the first to settle; any rejects with an AggregateError` only when all reject |
A Promise class | State machine (pending → fulfilled/rejected, once); then returns a new promise; handlers run asynchronously (microtask) even when already settled; a handler's return value resolves the next promise, a thrown error rejects it, a returned thenable is adopted | |
JSON.stringify | undefined/functions/symbols are omitted as object values and become null in arrays; NaN/Infinity → null; toJSON honoured (Dates); strings escaped; cycles throw TypeError; replacer function/array and indent | |
debounce/throttle | Shared timer across calls; trailing versus leading; cancel; preserve this and arguments | |
EventEmitter | on/off/once/emit; copy the listener list before emitting so off during emit is safe | |
| LRU cache | Map insertion order: delete + re-set on access; evict map.keys().next().value | |
deepClone | Primitives, arrays, plain objects, Date, RegExp, Map, Set; a WeakMap of seen objects for cycles and shared references; class instances lose their prototype unless you Object.create(Object.getPrototypeOf(x)) | |
deepEqual | Same type; NaN equals NaN; arrays by length and elements; objects by own key sets; Date by time; Map/Set by entries; cycles via a visited pair set if asked | |
curry/memoize/once/compose | Arity via fn.length; memo keys for multiple arguments; once caches the first result |
Worked shape: bind
Function.prototype.myBind = function (thisArg, ...preset) {
const target = this;
function bound(...args) {
// Called with `new`? Then `this` is the fresh instance and thisArg is ignored.
const useNew = this instanceof bound;
return target.apply(useNew ? this : thisArg, [...preset, ...args]);
}
bound.prototype = Object.create(target.prototype ?? null); // so instanceof and `new` keep working
return bound;
};
Interviewers probe exactly the new case and the partial-application case; mention both before they ask.
Worked shape: Promise.all
function all(iterable) {
return new Promise((resolve, reject) => {
const items = [...iterable];
const results = new Array(items.length);
let pending = items.length;
if (pending === 0) return resolve(results);
items.forEach((item, i) => {
Promise.resolve(item).then((value) => {
results[i] = value; // index, not push order
if (--pending === 0) resolve(results);
}, reject); // first rejection wins; later ones are ignored by the state machine
});
});
}
The two things people get wrong: pushing in completion order (breaks input order) and forgetting the empty case (never resolves).
Worked shape: a Promise
class MyPromise {
#state = "pending"; #value; #handlers = [];
constructor(executor) {
const settle = (state) => (value) => { if (this.#state !== "pending") return; this.#state = state; this.#value = value; this.#handlers.forEach((h) => h()); this.#handlers = []; };
try { executor(settle("fulfilled"), settle("rejected")); } catch (e) { settle("rejected")(e); }
}
then(onOk, onErr) {
return new MyPromise((resolve, reject) => {
const run = () => queueMicrotask(() => { // always asynchronous
const handler = this.#state === "fulfilled" ? onOk : onErr;
if (typeof handler !== "function") return this.#state === "fulfilled" ? resolve(this.#value) : reject(this.#value); // pass-through
try { const out = handler(this.#value); out && typeof out.then === "function" ? out.then(resolve, reject) : resolve(out); } // adopt thenables
catch (e) { reject(e); }
});
this.#state === "pending" ? this.#handlers.push(run) : run();
});
}
catch(onErr) { return this.then(undefined, onErr); }
}
The points that score: settling is one-shot; then returns a new promise; handlers run in a microtask even when the promise is already settled; a missing handler passes the value/reason through; returned thenables are adopted. resolve(thenable) inside the executor should adopt too — mention it if you skip it.
Worked shape: reduce with holes and the empty case
Array.prototype.myReduce = function (fn, ...seed) {
let i = 0, acc;
if (seed.length) acc = seed[0];
else { while (i < this.length && !(i in this)) i++; if (i >= this.length) throw new TypeError("Reduce of empty array with no initial value"); acc = this[i++]; }
for (; i < this.length; i++) if (i in this) acc = fn(acc, this[i], i, this);
return acc;
};
Using rest for the seed distinguishes "no seed" from "seed is undefined" — the arity check the spec makes with arguments.length.
How to answer these live
- Say the contract first — what the built-in guarantees — in three or four bullets, including the edge case you know the interviewer is thinking of.
- Write the simple path.
- Add the edge cases one at a time, naming each ("now the
newcase"). - Test with two calls: the normal one and the edge one.
If you do not know a detail, say what you would check ("I believe map skips holes; I'd confirm on MDN"). Guessing confidently and wrong is the worst outcome.
Common mistakes
bindthat ignoresnew;newthat ignores an object returned by the constructor.map/filteronarr.lengthwithouti in arr;reduceseeded withundefinedinstead of checking arity.Promise.allpushing in completion order or never resolving on[];thenthat runs handlers synchronously.JSON.stringifywritingundefinedor leavingNaN; forgettingtoJSON.deepClonewithout a seen-map (infinite recursion on cycles);deepEqualthat fails onNaNor comparesDates by identity.
What the interviewer is listening for
- Whether you state the contract before coding, and whether it includes the famous edge case.
- Whether the asynchronous ones stay asynchronous and one-shot.
- Whether the array ones respect holes,
thisArgand the callback signature. - Whether you test the edge case unprompted.
Key takeaways
- The recurring reimplementation questions each hinge on one or two spec details — know the detail, not just the sketch.
bindandnew;mapand holes;reduceand the empty array;Promise.alland order/empty;thenand microtasks/adoption;JSON.stringifyandundefined/NaN/toJSON/cycles.- Answer in the order contract → simple path → edge cases named one by one → two tests.
- Say what you would verify rather than guessing a detail.
Common questions
How do you implement bind in JavaScript?
Return a function that calls the target with apply, passing the bound this and the preset arguments followed by the new ones. The detail interviewers probe: when the bound function is called with new, the bound this is ignored — detect it with this instanceof bound, and link the bound function's prototype to the target's.
How do you write a Promise from scratch?
As a state machine that settles once, from pending to fulfilled or rejected. then returns a new promise; its handlers run in a microtask even when the promise has already settled; a missing handler passes the value or reason through; a thrown error rejects the next promise; and a returned thenable is adopted.
Why does reduce throw on an empty array?
With no initial value, reduce starts from the first present element, and an empty array has none, so it throws TypeError: Reduce of empty array with no initial value. A correct reimplementation checks whether a seed was passed at all — the argument count — rather than whether it is undefined.
What does JSON.stringify do with undefined, functions and NaN?
As object values, undefined, functions and symbols are left out; inside arrays they become null. NaN and Infinity are written as null, a toJSON method such as Date's is honoured, and a circular reference throws a TypeError.
What is the difference between includes and indexOf?
includes compares with SameValueZero, so [NaN].includes(NaN) is true; indexOf uses strict equality, so [NaN].indexOf(NaN) is -1. Both treat +0 and -0 as equal; includes returns a boolean, indexOf the position or -1.
Exercises
bind, call, apply, new and instanceof by hand
Implement Function.prototype.myCall/myApply (bind this through a temporary symbol-keyed method), myBind (pre-applied arguments and the new case — a bound function called with new ignores the bound this and keeps the prototype chain), myNew(Ctor, …args) (an object returned by the constructor replaces this) and myInstanceOf(value, Ctor) (walk the prototype chain; primitives and null are false). The starter's checks compare each against the native behaviour; stdin lists check names or all. Print ok <check>: <detail> match=true per check and <n>/<total> match the native behaviour.
Example (all) →
ok call: hi, Ada! match=true
ok apply: 9 match=true
ok bind-partial: add(10)(5)=15 match=true
ok bind-this: n=2 match=true
ok bind-new: {x:1,y:2} instanceof Point=true sum=3 match=true
ok new-plain: {x:1,y:2} sum=3 match=true
ok new-returns-object: {"custom":true} match=true
ok instanceof: dog->Dog,Animal,Object,Array | [],5,null,nullProto->Object = true,true,true,false,true,false,false,false match=true
8/8 match the native behaviourThe array methods from scratch
Line 1 lists elements — numbers, NaN, words, JSON arrays like [3,[4,[5]]] and _ for a hole. Implement myForEach, myMap, myFilter, myReduce, myFlat(depth = 1), myFlatMap, myIncludes and myIndexOf on Array.prototype with the spec details: holes are skipped by the callbacks (map keeps them as holes), thisArg is honoured, reduce without a seed starts from the first present element and throws TypeError on an empty array, includes uses SameValueZero (finds NaN, reads a hole as undefined) while indexOf uses === and skips holes. The starter's show renders holes as <hole>; print each comparison as <label>: <mine> match=<bool>.
Example: 1 _ 2 NaN hello [3,[4,[5]]] _ → map x2: [2,<hole>,4,NaN,"hello",[3,[4,[5]]],<hole>] match=true, flat(Infinity): [1,2,NaN,"hello",3,4,5] match=true, includes(NaN): true match=true, indexOf(NaN): -1 match=true, includes(undefined) (a hole reads as undefined): true match=true.
In this module: Interview idioms
- The template — running a JavaScript coding round from your side of the table
- The patterns — the dozen algorithm shapes behind most JavaScript rounds
- The idiom sheet — the one-liners worth knowing by heart, and the traps inside them
- Implement the built-in — the reimplementation questions and the details that decide them (this lesson)
- Theory drill — the questions, the two-sentence answers, and where each one goes deeper
- Checkpoint — Interview idioms
← The idiom sheet — the one-liners worth knowing by heart, and the traps inside them · Theory drill — the questions, the two-sentence answers, and where each one goes deeper →