Concept
Machine-coding rounds ask you to implement a small, self-contained utility from scratch, live, usually in 20-40 minutes, no framework, no libraries, just the language. The specific set that recurs across almost every frontend machine-coding rotation is small and well-known: debounce, throttle, a minimal pub/sub event emitter, deepClone, and one or more Promise polyfills (Promise.all, Promise.race, or a full myPromise implementing the constructor and .then() chaining itself). This topic doesn't re-teach each of these from a blank page, this platform already has two better places for that: full, narrated "Implement It Yourself" walkthroughs embedded in the relevant JS-domain topics, and The Forge, this platform's runnable, auto-graded code-practice feature, which has exactly these patterns as real, gradable-in-the-browser challenges. What this topic adds is the interview-specific framing: why these five patterns keep getting asked, what interviewers are actually listening for as you talk through your implementation, and the traps that separate a "looks right" answer from a genuinely correct one.
Why this specific set of patterns
All five patterns share a common thread: they force you to demonstrate real understanding of closures, the event loop, and reference-vs-value semantics, the exact JS fundamentals that are easy to use correctly in application code without ever having to explain them out loud. debounce and throttle both test whether you understand closures (the timer/flag state has to persist between calls to the returned function) and the event loop (you're reasoning explicitly about setTimeout and when callbacks actually fire). An event emitter tests whether you understand how to model a simple pub/sub registry with a plain object or Map and correctly handle removal (off) without breaking iteration over still-registered handlers. deepClone tests recursion over heterogeneous nested structures and forces you to reckon with what "equal" even means for objects, arrays, dates, and cyclic references. Promise polyfills test whether you understand what a promise is doing under the hood, states, the microtask queue, chaining, rather than just calling .then() correctly in application code.
Go implement these yourself, gradable in the browser, before or after reading the condensed version below, The Forge has all three of the core JS versions of these as real challenges with automated test suites:
/forge/js.debounce, implement debounce with full test coverage for the "resets on every call" behavior./forge/js.throttle, implement throttle, graded against the "fires immediately, then rate-limits" behavior that trips up people who confuse it with debounce./forge/js.event-emitter, implementon/off/emit, graded against removal-during-iteration edge cases.
(deepClone and Promise-polyfill challenges also exist in Forge under the ids js.deep-clone, poly.my-promise, and poly.promisify, same idea: write it, run the grader, see exactly which test case fails and why, which is a faster feedback loop than reading an explanation of someone else's implementation.)
debounce vs throttle, the pairing that's always asked together
These two are asked together specifically because they're easy to confuse and the difference is the entire point of the question:
debounce(fn, delay): waits for a PAUSE in calls before firing.
Every call resets the timer. Fires once, delay ms after the LAST call.
Use case: search-as-you-type (only fire the API call once typing stops).
throttle(fn, interval): fires IMMEDIATELY on first call, then ignores
further calls until interval ms have passed, then allows the next
call through immediately again.
Use case: scroll/resize handlers (guarantee a MAXIMUM call rate,
not "wait for quiet").The interview-critical distinction: debounce guarantees the function fires after activity stops; throttle guarantees the function fires at most once per interval, even during continuous activity. Confusing these in an interview (e.g. describing throttle's behavior while calling it debounce) is one of the most common, and most quickly disqualifying, mistakes in this rotation, precisely because the difference is small in code but large in behavior.
Event emitter, the pattern behind pub/sub everywhere
A minimal event emitter is three methods over a registry mapping event names to arrays of handlers, on pushes a handler, off removes one, emit calls every registered handler for an event name with the given arguments. The subtlety interviewers probe for is what happens when a handler calls off on itself (or another handler) during an in-progress emit, iterating a live array while mutating it can skip or double-invoke handlers, which is why a careful implementation iterates over a shallow copy of the handler array inside emit, not the live array itself.
deepClone, recursion over heterogeneous structures
deepClone needs to recurse through nested plain objects and arrays, deciding at each node whether to copy primitively or recurse further, and critically must not lose the original's structure for special cases: Date objects need new Date(value.getTime()), not a shallow property copy; arrays need Array.isArray handling separate from generic objects; and a genuinely complete version also needs a way to avoid infinite recursion on cyclic references (an object that (directly or indirectly) contains a reference to itself), typically solved with a WeakMap tracking already-cloned objects.
Promise polyfills, proving you understand what .then() actually does
The lightest version of this ask is implementing Promise.all or Promise.race using only native promises (testing whether you understand how to coordinate multiple async operations and correctly propagate the first rejection). The heaviest version is a full myPromise class implementing the constructor's executor pattern, the pending/fulfilled/rejected state machine, and .then() itself queuing callbacks onto the microtask queue rather than invoking them synchronously, this is the single hardest common machine-coding question specifically because getting .then() chaining and asynchronous callback scheduling right by hand requires genuinely understanding, not just using, what a promise is.
Try It
Predict the console output, paying attention to WHICH pattern (debounce or throttle) is at play and when each logged call actually fires.
function throttle(fn, interval) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= interval) {
lastCall = now;
fn(...args);
}
};
}
const log = throttle((n) => console.log("call:", n), 100);
log(1); // t=0
Solution
Output:
call: 1
call: 4At t=0, lastCall is 0 and now - lastCall >= 100 is true, so log(1) fires immediately and lastCall becomes ~0. At t=30 and t=60, now - lastCall is only 30 and 60 respectively, both less than the 100ms interval, so log(2) and log(3) are silently dropped, not queued or delayed, just ignored. At t=150, now - lastCall is ~150, which is ≥ 100, so log(4) fires. This is the key throttle behavior that trips people up: dropped calls during the "cooldown" window are gone entirely, not deferred to fire later, that deferred-trailing-call behavior is a valid throttle variant (trailing-edge throttle) but not what this specific, most commonly-asked leading-edge implementation does.
Implement It Yourself
This is a condensed version, for the full, gradable, test-suite-backed versions of debounce/throttle/event-emitter specifically, use the Forge links in the Concept section above; this is meant as a quick reference/warm-up, not a replacement for actually writing and running them.
// debounce, condensed
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// throttle, condensed (leading-edge variant)
function throttle(fn, interval) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now
Try extending deepClone to handle a cyclic reference (const a = {}; a.self = a;) using a WeakMap to track already-cloned objects before attempting the full graded version on Forge.
Under the Hood
The closure mechanics underlying debounce, throttle, and the event emitter's private events registry are covered in full in Closures. The timer-scheduling behavior underlying debounce and throttle, specifically why a setTimeout callback never fires earlier than requested but can fire later, and how this relates to the microtask/macrotask distinction relevant to Promise polyfills, is covered in The Event Loop. For the runnable, graded versions of these exact patterns: /forge/js.debounce, /forge/js.throttle, /forge/js.event-emitter.
Common Mistakes
1. Confusing debounce and throttle out loud
"Throttle waits for the user to stop typing, then fires once." // ❌
// That's debounce's behavior, not throttle's.This single mix-up is the most common, most quickly-noticed mistake in this entire rotation, interviewers listen for this specifically because it reveals whether you actually understand the behavior or are pattern-matching on the name.
2. Iterating the live handler array in emit
// ❌ mutating `events[event]` (via a handler calling off()) WHILE
// this loop is iterating over the exact same live array
for (const handler of events[event] ?? []) handler(...args);If any handler calls off() on itself or another handler during emit, mutating the array mid-iteration can skip the next handler or produce inconsistent behavior, iterate over a shallow copy ([...events[event]]) instead.
3. Shallow-copying deepClone special cases
// ❌ spreading a Date "clones" it into a plain object, losing its Date-ness
function badClone(value) {
if (Array.isArray(value)) return [...value];
if (typeof value === "object") return { ...value }; // shallow AND loses Date methods
return value;
}{ ...someDate } produces a plain object with no Date methods at all, not a cloned Date, special-case Date (and other built-ins like Map/Set if the prompt requires them) explicitly rather than assuming a generic object spread handles everything.
4. Implementing a Promise polyfill's .then() synchronously
// ❌ invoking the callback immediately/synchronously instead of
// deferring it to the microtask queue
then(onFulfilled) {
if (this.state === "fulfilled") onFulfilled(this.value); // wrong, runs sync
}Real promises always resolve callbacks asynchronously (via the microtask queue), even if the promise is already settled at the time .then() is called, a synchronous implementation breaks ordering guarantees relative to other code, which is exactly the subtlety this question is testing.
Best Practices
- State the debounce-vs-throttle distinction out loud before coding, even if not asked directly, it preempts the single most common confusion point in this rotation.
- Iterate over a copy of the handler array inside
emit, not the live registry, to stay correct under handlers that mutate the registry mid-emit. - Special-case
Date(and ask aboutMap/Set/cycles) indeepClonerather than assuming a generic recursive object/array copy covers everything. - Defer Promise polyfill callbacks asynchronously, matching real Promise microtask semantics, rather than invoking them synchronously for convenience.
- Use the actual Forge challenges to practice these under real grading rather than only reading through implementations, writing it yourself and seeing which test case fails is a meaningfully different (and stronger) learning signal than reading a finished solution.
Performance Tips
- Debounce and throttle both trade a small amount of latency (the delay/interval window) for a large reduction in call volume, frame this trade-off explicitly when asked "why not just call the function directly," since interviewers sometimes probe whether you understand it's a deliberate trade-off, not a free optimization.
- For very hot event sources (scroll, resize, mousemove), throttle is almost always the better default over debounce, debounce can indefinitely delay the FIRST response if activity never fully stops, which is undesirable for something like a scroll-position indicator that should update continuously, just at a bounded rate.
- A
WeakMap-based cycle guard indeepCloneadds a small constant overhead per object but turns an infinite-recursion crash on cyclic input into correct, defined behavior, worth the cost for any clone utility that might see real-world (not just interview-clean) data.
