Implement a debounce function from scratch
Return a function that resets a timer on every call and only invokes fn after `wait` ms of silence. Forward `this` and arguments, expose cancel/flush, and optionally support a leading-edge call.
Debounce is the most-asked utility in frontend interviews because it tests four things at once: closures, this binding, timers, and API design. The naive answer (setTimeout in a closure) gets you 60% credit; the rest is what a senior adds — this forwarding, cancel/flush, leading vs trailing, and edge cases.
Definition. A debounced function delays invoking fn until wait ms have elapsed since the last call. Useful for: search-as-you-type, autosave, resize handlers, expensive recalculation on scroll.
Trailing-edge (default) implementation. Hold the timer in closure. Each invocation clears the previous and schedules a new one.
this and arguments forwarding. Because the user might use the debounced function as a method (obj.handler = debounce(fn, 200)) or pass it as a DOM event handler, you must use a function expression (not arrow) and call fn.apply(this, args) so the original this and the latest args are preserved.
Cancel. Expose .cancel() to drop the pending invocation — important for cleanup in React's useEffect return, or when the user navigates away.
Flush. .flush() invokes the pending call immediately and clears the timer — useful when a form submits and you want the latest debounced value applied right now.
Leading edge. With { leading: true } the first call fires immediately, then subsequent calls within wait are suppressed; the trailing call fires after silence. Common pattern for "only fire once per burst."
Debounce vs throttle. Debounce waits for silence; throttle guarantees one call per interval. Use debounce when you only care about the final value (search), throttle when you need steady updates (scroll-position UI).
React gotcha. Don't debounce(fn, 200) inline in render — every render produces a new debounced function and the timer never accumulates. Wrap in useMemo (or use useDebouncedCallback from use-debounce) and call .cancel() in the cleanup.
Code
Follow-up questions
- •Implement throttle — how does it differ?
- •How would you support both leading and trailing options together?
- •Why does debouncing a method without preserving `this` break?
- •How does lodash.debounce handle maxWait?
Common mistakes
- •Using an arrow function for the returned wrapper — `this` is lost.
- •Forgetting to capture `args` in the closure (using outer `arguments`).
- •Calling `debounce` inline in JSX — new instance every render, timer never accumulates.
- •Not exposing cancel — leaks pending timers when the component unmounts.
Performance considerations
- •Debouncing avoids unnecessary work but adds latency equal to `wait`. Pick wait based on UX (250ms feels instant for search).
- •For very high-frequency events, throttle keeps the UI smoother than debounce.
Edge cases
- •wait = 0 still defers via setTimeout — useful to coalesce within a tick.
- •Calling cancel after flush is a no-op (timer is already cleared).
- •If `fn` throws, the timer is cleared but the error propagates synchronously from the timer callback.
Real-world examples
- •Lodash's _.debounce, the use-debounce React hook, and most search inputs in production apps.