Problem Statement
In high-frequency event contexts (search input streams, window resize handlers, scroll events), debouncing is essential to avoid excessive function calls.
Implement a custom debounce function with the following capabilities:
const debounced = debounce(fn, delay, options);
Requirements
- Core behavior: The returned function delays invoking
fnuntildelayms have elapsed since the last call. - Context & Arguments: Preserve
thisbinding and forward all arguments tofn. - Leading edge execution (
options.immediate):- If
true, triggerfnimmediately on the first call - Subsequent calls during the delay window are suppressed
- After the delay expires with no calls, the next call triggers immediately again
- If
- Cancellation: The returned function must have a
.cancel()method that clears any pending timer and resets internal state.
Constraints
- No external libraries (no Lodash)
- Must work in both browser and Node.js environments
- Should handle rapid successive calls correctly
- Edge case: calling
.cancel()when nothing is pending should not throw
Example
const log = debounce(console.log, 300);
log("a"); // nothing yet
log("b"); // resets timer
log("c"); // resets timer
// 300ms later → logs "c"
const logImmediate = debounce(console.log, 300, { immediate: true });
logImmediate("x"); // logs "x" immediately
logImmediate("y"); // suppressed
logImmediate("z"); // suppressed
// 300ms of silence...
logImmediate("w"); // logs "w" immediately (delay expired)
logImmediate.cancel(); // clears any pending execution
What the Interviewer Expects
- Closure understanding — timer ID and state must persist across calls via closure.
thisbinding — usefn.apply(context, args)or arrow functions correctly. Common trap: arrow functions insidedebouncelose the caller'sthis.- Leading vs trailing edge logic:
- Trailing (default): set timer on each call, only the last one fires
- Leading: fire immediately if no timer is active, then start suppression window
- Cancel implementation:
clearTimeout(timerId)+ reset the timer variable. - Return value handling (bonus): for immediate mode, the debounced function could return
fn's result.
Follow-ups
- How would you add a
flush()method that executes the pending function immediately? - What's the difference between debounce and throttle? When would you use each?
- How would you implement a
maxWaitoption — guaranteefnis called at least once every N ms even if events keep coming? - How does React's
useCallbackinteract with debounced functions? What's the common bug? - How would you unit test this? What timing strategies would you use (fake timers)?