Frontend
easy
mid
Implement a basic debounce function without using any library.
A closure over a timer id: each call clears the pending timeout and schedules a new one, so the function runs only after calls stop for the wait period. Preserve this/arguments with apply, and add a cancel method for cleanup.
5 min read·~10 min to think through
Debounce delays a function until calls stop for wait ms — every new call resets the timer.
The implementation
js
function debounce(fn, wait) {
let timeoutId; // closure-held timer id
return function (...args) {
clearTimeout(timeoutId); // cancel any pending call
timeoutId = setTimeout(() => {
fn.apply(this, args); // run later, preserving this + args
}, wait);
};
}How it works:
debouncereturns a new function that closes overtimeoutId.- Each call clears the previous pending timeout and schedules a new one.
fnonly fires when calls stop forwaitms — every call within the window pushes it back.fn.apply(this, args)forwards the call context and arguments correctly.
Usage
js
const onResize = debounce(() => console.log("resized"), 200);
window.addEventListener("resize", onResize);
const search = debounce((q) => fetchResults(q), 300);
input.addEventListener("input", (e) => search(e.target.value));Add a cancel method (the senior touch)
Important so you can clean up a pending call (e.g. on React unmount):
js
function debounce(fn, wait) {
let timeoutId;
function debounced(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), wait);
}
debounced.cancel = () => clearTimeout(timeoutId);
return debounced;
}Things to get right (and mention)
this/arguments— use a regular function +apply, not an arrow, so the caller'sthisis preserved. (...argscaptures arguments.)- Closure —
timeoutIdmust persist across calls; it lives in the closure, one per debounced function. cancel()— needed for cleanup.- Optional extensions: a
leadingoption (fire on the first call, then debounce),trailing(the default), or returning the result via a promise.
Debounce vs throttle (clarify if asked)
- Debounce — runs once after calls stop. Search input, autosave, resize-settled.
- Throttle — runs at most once per interval during continuous calls. Scroll, mousemove, drag.
How to answer
"Return a function that closes over a timer id; each call clears the pending timeout and sets a new one, so fn runs only after calls quiet down for wait ms. Use a regular function with apply to preserve this and arguments, and attach a cancel method so a pending call can be cleared on cleanup."
Follow-up questions
- •Why use a regular function and apply instead of an arrow function here?
- •Why does the debounced function need a cancel method?
- •How would you add a leading-edge option?
- •How is this different from throttle?
Common mistakes
- •Using an arrow function for the returned function, losing the caller's this.
- •Declaring timeoutId inside the returned function — it resets every call, so it never debounces.
- •Not providing a cancel method for cleanup.
- •Confusing debounce with throttle.
Performance considerations
- •Debounce collapses a burst of events into one execution — essential for API calls, expensive computations, and resize/scroll handlers. The wait value trades responsiveness against work done.
Edge cases
- •Caller relies on this — must be preserved via apply.
- •Pending call when the consumer needs to clean up (cancel).
- •leading + trailing behavior.
- •Very rapid calls that should still result in exactly one execution.
Real-world examples
- •Search-as-you-type, autosave-on-pause, resize handlers, form validation on input-settled.
Senior engineer discussion
Seniors write it cleanly with a closure-held timer, correctly preserve this/arguments with a regular function + apply, and add cancel for cleanup — then mention leading/trailing options and contrast debounce vs throttle by use case. They know the classic bug: declaring the timer id in the wrong scope.
Related questions
Frontend
Medium
6 min
Frontend
Medium
5 min