Back to JavaScript
JavaScript
medium
mid

How would you implement debouncing in JavaScript?

Debounce delays running a function until it stops being called for a set wait time — each call resets a timer. Implement with a closure over a timer id: clear the previous timeout and set a new one. Used for search-as-you-type, autosave, and resize handlers.

6 min read·~12 min to think through

Debouncing ensures a function runs only after a pause in calls — every new call resets the wait timer. "Wait until the user stops doing the thing, then run once."

Core implementation

js
function debounce(fn, wait) {
  let timeoutId;
  return function (...args) {
    clearTimeout(timeoutId);                       // cancel the pending call
    timeoutId = setTimeout(() => fn.apply(this, args), wait); // schedule a new one
  };
}

// usage
const onSearch = debounce((q) => fetchResults(q), 300);
input.addEventListener("input", (e) => onSearch(e.target.value));

The mechanics:

  • A closure holds timeoutId across calls.
  • Each call clears the previous pending timeout and sets a new one.
  • fn only fires once calls stop for wait ms.
  • apply(this, args) preserves this and forwards arguments.

Production-quality version — add the useful options

js
function debounce(fn, wait, { leading = false, trailing = true } = {}) {
  let timeoutId;
  function debounced(...args) {
    const callNow = leading && !timeoutId;
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      timeoutId = null;
      if (trailing && !callNow) fn.apply(this, args);
    }, wait);
    if (callNow) fn.apply(this, args);
  }
  debounced.cancel = () => { clearTimeout(timeoutId); timeoutId = null; };
  return debounced;
}
  • leading — fire immediately on the first call, then debounce the rest.
  • trailing — fire after the pause (the default).
  • cancel() — important so React effects can clean up a pending call on unmount.

Debounce vs throttle (always clarify)

  • Debounce — waits for calls to stop; fires once after quiet. Search input, autosave, validation, resize-settled.
  • Throttle — fires at most once per interval during continuous calls. Scroll handlers, mousemove, drag, rate-limiting.

"Debounce = run after the storm; throttle = run every N ms during the storm."

Using it in React

jsx
const debouncedSearch = useMemo(() => debounce(search, 300), []);
useEffect(() => () => debouncedSearch.cancel(), [debouncedSearch]); // cleanup
  • useMemo so it isn't recreated each render (a fresh debounce loses its timer).
  • Cancel on unmount or a pending call fires on a gone component.
  • Or use a useDebounce hook that debounces a value via setTimeout in an effect.

Edge cases / gotchas

  • Recreating the debounced fn each render resets its state — memoize it.
  • Not cancelling on unmount → "setState on unmounted component" / stale call.
  • this binding — apply handles it; arrow-function gotchas if you're careless.
  • Leading+trailing interaction needs care (handled above).

Follow-up questions

  • What's the difference between debounce and throttle, and when do you use each?
  • How do you debounce correctly inside a React component?
  • Why does the debounced function need a cancel method?
  • What do leading and trailing options do?

Common mistakes

  • Recreating the debounced function every render so it never actually debounces.
  • Not cancelling the pending call on unmount.
  • Confusing debounce with throttle.
  • Losing this/arguments by not using apply.

Performance considerations

  • Debouncing collapses a burst of events into a single execution — critical for API calls (search-as-you-type), expensive computations, and validation. The wait value tunes responsiveness vs. work: too short and it barely helps, too long and it feels laggy.

Edge cases

  • Component unmounts with a call still pending.
  • leading + trailing both enabled.
  • The debounced function needs to be cancelled or flushed immediately.
  • Rapid calls that should still eventually run exactly once.

Real-world examples

  • Search-as-you-type firing one API call after the user pauses.
  • Autosave after the user stops typing; validating a form field on input-settled.

Senior engineer discussion

Seniors implement it cleanly with a closure, then extend to leading/trailing/cancel and explain the React pitfalls (memoize the instance, cancel on unmount). They always contrast debounce vs throttle by use case, and know debounce-the-value (effect + timeout) as the idiomatic React-hook alternative.

Related questions