“Implement debounce.” “Write your own Promise.all.” “Build an EventEmitter.” These questions dominate the JavaScript round because a single small utility exercises closures, higher-order functions, async timing, and edge-case discipline all at once. The good news: they are far more alike than they look, and one method handles the whole family.
Why interviewers love them
A from-scratch utility is a compact test of whether you understand the language rather than the library. Rebuilding debounce forces you to hold state across calls (a closure), return a new function (a higher-order function), manage a timer, and decide what happens to this and the arguments — the exact concepts a senior filter is looking for. That is why the same handful of utilities recur: they are dense with signal.
Start with the contract, not the code
Before writing a line, state the function’s shape out loud: what it takes, what it returns, and how it behaves over time. “debounce(fn, wait) returns a new function; calling it resets a timer, and fn runs only once calls stop for wait milliseconds.” This contract is where the marks are. It proves you understand the behaviour, and it surfaces the questions that separate strong answers from passing ones: does the debounced function preserve this and its arguments? Is there a leading-edge call or only a trailing one? Can it be cancelled? Ask these before coding — raising them unprompted is a senior signal.
The method that works for all of them
- Name the private state. Almost every utility keeps something between calls in a closure: a timer id (
debounce/throttle), a “have I run” flag (once), a cache (memoize), a results array and a counter (Promise.all), a map of listeners (EventEmitter). Identify it first; the rest of the code exists to read and update it. - Return the right thing. Decide immediately whether you return a wrapped function, a promise, or an object with methods, and preserve
thisandargumentswhen you wrap — use a regular function andfn.apply(this, args), not an arrow that drops the receiver. - Handle the edges you named. Empty input (
Promise.all([])resolves to[]immediately), a single rejection (rejects the whole thing on the first error, preserving order of results), double invocation, cleanup on cancel. - Dry-run it. Trace one real sequence of calls out loud — “call, call 5ms later, then wait” — to prove the timing does what the contract promised.
See the family, not the instances
Once you frame these as “state in a closure + a returned wrapper + edge cases,” the individual questions stop being separate things to memorise. throttle is debounce with a different timing rule; once is a boolean flag; memoize is a cache keyed by arguments; Promise.all is a counter that resolves when it hits the input length. Learning the method means you can derive the one they ask even if it is a variant you have not seen — which is the entire point of the round. This is closures doing real work; if that part feels shaky, read the closure mental model first.
Drill the whole family on the JavaScript interview questions — the “implement from scratch” and async questions there are grouped into a learning path, and each ships runnable code you can edit and break to see the edge cases for yourself.