JavaScript
JS Deep-Dive: Promise Polyfills + Debounce/Throttle
Difficulty: 🟡 Medium · Est. time:
1.5h· Tags:#polyfill#async#patterns
Asked at: Google, Meta, Amazon, Uber (near-universal) · Related: Autocomplete · Event Bus
1. Why this matters
debounce, throttle, and the Promise.* combinators are the most-asked "implement this from scratch" questions in frontend loops. They test async understanding, closures, this, and edge-case thinking.
2. Debounce
Definition: delay invoking fn until wait ms have passed since the last call. Great for search inputs, resize, autosave.
function debounce(fn, wait = 300, { leading = false } = {}) {
let timer = null;
let calledLeading = false;
function debounced(...args) {
const runLeading = leading && !timer && !calledLeading;
if (timer) clearTimeout(timer);
if (runLeading) {
fn.apply(this, args);
calledLeading = true;
}
timer = setTimeout(() => {
if (!leading) fn.apply(this, args); // trailing edge
timer = null;
calledLeading = false;
}, wait);
}
debounced.cancel = () => { clearTimeout(timer); timer = null; calledLeading = false; };
return debounced;
}
Talk about: preserving this (apply), forwarding args, cancel() for cleanup, leading vs trailing edge.
3. Throttle
Definition: invoke fn at most once per wait ms. Great for scroll, mousemove, rAF-like sampling.
function throttle(fn, wait = 300) {
let last = 0;
let timer = null;
return function throttled(...args) {
const now = Date.now();
const remaining = wait - (now - last);
if (remaining <= 0) { // leading edge
if (timer) { clearTimeout(timer); timer = null; }
last = now;
fn.apply(this, args);
} else if (!timer) { // trailing edge
timer = setTimeout(() => {
last = Date.now();
timer = null;
fn.apply(this, args);
}, remaining);
}
};
}
Debounce vs throttle: debounce waits for quiet; throttle guarantees a steady rate. Search box → debounce. Scroll position → throttle.
4. Promise polyfill (Promises/A+ shape)
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.cbs = [];
const resolve = (v) => this.#settle('fulfilled', v);
const reject = (e) => this.#settle('rejected', e);
try { executor(resolve, reject); } catch (e) { reject(e); }
}
#settle(state, value) {
if (this.state !== 'pending') return;
this.state = state; this.value = value;
queueMicrotask(() => this.cbs.forEach(cb => cb())); // microtask timing
}
then(onF, onR) {
return new MyPromise((resolve, reject) => {
const run = () => {
try {
if (this.state === 'fulfilled') resolve(onF ? onF(this.value) : this.value);
else reject(onR ? onR(this.value) : this.value);
} catch (e) { reject(e); }
};
this.state === 'pending' ? this.cbs.push(run) : queueMicrotask(run);
});
}
}
Talk about: microtask timing (queueMicrotask), state machine (pending→settled once), chaining. (Full A+ also handles thenable assimilation.)
5. Promise combinators
// all: resolves with array; rejects on first rejection
function all(promises) {
return new Promise((resolve, reject) => {
const out = []; let done = 0; const list = [...promises];
if (!list.length) return resolve(out);
list.forEach((p, i) =>
Promise.resolve(p).then(v => { out[i] = v; if (++done === list.length) resolve(out); }, reject));
});
}
// allSettled: never rejects; reports each outcome
function allSettled(promises) {
return Promise.all([...promises].map(p =>
Promise.resolve(p).then(
value => ({ status: 'fulfilled', value }),
reason => ({ status: 'rejected', reason }))));
}
// race: settles with the first to settle
function race(promises) {
return new Promise((resolve, reject) =>
[...promises].forEach(p => Promise.resolve(p).then(resolve, reject)));
}
// any: first fulfillment; rejects with AggregateError if all reject
function any(promises) {
return new Promise((resolve, reject) => {
const errs = []; let rejected = 0; const list = [...promises];
if (!list.length) return reject(new AggregateError([], 'All promises were rejected'));
list.forEach((p, i) =>
Promise.resolve(p).then(resolve, e => {
errs[i] = e; if (++rejected === list.length) reject(new AggregateError(errs));
}));
});
}
Order-safety: write results by index, not push — all must preserve input order regardless of resolution order.
6. What Interviewers Probe
- Debounce vs throttle — when each, and the leading/trailing edge.
- Preserving
thisand arguments; addingcancel(). - Promise microtask timing vs
setTimeout. - Implementing
allorder-safely;allSettledvsall;anyvsrace. - Cancellation (AbortController) layered on top.
7. Curated Resources
- javascript.info: promises ⭐ · microtasks
- Promises/A+ spec
- MDN: Promise.all / allSettled / any / race
- MDN: queueMicrotask
8. Related Topics
- Design Autocomplete — debounce in the wild
- Event Bus
- JavaScript section