Back to JavaScript
JavaScript
easy
mid

How would you write a custom debounce function in JavaScript?

Debounce: delay invoking until N ms have passed since the last call. Implementation captures a timer in a closure; each call clears the prior timer and schedules a new one. Variants: leading edge (call immediately, then ignore), trailing (default), `flush` / `cancel` methods, AbortSignal support.

3 min read·~12 min to think through

Basic implementation

js
function debounce(fn, ms) {
  let t;
  return function (...args) {
    clearTimeout(t);
    t = setTimeout(() => fn.apply(this, args), ms);
  };
}

A closure captures t. Each call clears the previous timer and schedules a new one. The function only fires after ms of quiet.

With leading + trailing edges (lodash-style)

js
function debounce(fn, ms, { leading = false, trailing = true } = {}) {
  let t = null;
  let lastArgs = null;
  let lastThis = null;
  let result;

  function invoke() {
    result = fn.apply(lastThis, lastArgs);
    lastArgs = lastThis = null;
  }

  function debounced(...args) {
    const callNow = leading && !t;
    lastArgs = args;
    lastThis = this;

    if (t) clearTimeout(t);
    t = setTimeout(() => {
      t = null;
      if (trailing && lastArgs) invoke();
    }, ms);

    if (callNow) invoke();
    return result;
  }

  debounced.cancel = () => { if (t) clearTimeout(t); t = null; lastArgs = lastThis = null; };
  debounced.flush = () => { if (t) { clearTimeout(t); t = null; if (lastArgs) invoke(); } };
  return debounced;
}

Use cases

  • Input change triggering search → wait until typing pauses.
  • Resize handler → wait until resize stops, then recompute.
  • Save draft → wait until typing pauses, then auto-save.

Debounce vs throttle

  • Debounce — wait for quiet. Last call wins.
  • Throttle — at most one call per N ms. First (or scheduled) call within window wins.

Use debounce for "do this when the user stops"; throttle for "do this at most this often."

React + debounce gotchas

Don't create a new debounced function every render:

jsx
// BAD — new function per render; debounce state is reset every time
const onSearch = debounce(search, 250);

// GOOD
const onSearch = useMemo(() => debounce(search, 250), []);
useEffect(() => () => onSearch.cancel(), []);   // cleanup

Or use a ref:

jsx
const debouncedRef = useRef();
useEffect(() => {
  debouncedRef.current = debounce(search, 250);
  return () => debouncedRef.current.cancel();
}, []);

AbortSignal-aware variant

js
function debounce(fn, ms, { signal } = {}) {
  let t;
  signal?.addEventListener("abort", () => clearTimeout(t), { once: true });
  return (...args) => {
    clearTimeout(t);
    if (signal?.aborted) return;
    t = setTimeout(() => fn(...args), ms);
  };
}

Common mistakes

  • Calling debounce inside a render → new fn every time → never debounces.
  • Forgetting to clean up the pending timer on unmount.
  • Confusing debounce with throttle in the answer.
  • Losing this by using arrow inside the wrapper (use function if this matters).

Interview framing

"Closure capturing the timer; each call clears the prior timer and schedules a new one — the function only fires after the configured quiet period. Useful for search input, resize handlers, autosave. Add cancel and flush methods for cleanup and flush-on-demand. Leading edge calls immediately on the first invocation in a quiet period — handy when you want a fast first response. In React, memoize the debounced function (useMemo or a ref) so a new one isn't created every render. Don't confuse with throttle — debounce waits for quiet, throttle caps frequency."

Follow-up questions

  • Compare debounce vs throttle.
  • How would you support leading + trailing?
  • Why does React render create bugs with debounce?

Common mistakes

  • Recreating debounced fn per render.
  • No cleanup on unmount.
  • Confusing debounce and throttle.

Performance considerations

  • Cheap — one setTimeout in flight. Massive win for hot handlers (resize, scroll).

Edge cases

  • Leading + trailing both fire on quick pulses.
  • Cancel during the wait.
  • this binding for methods.

Real-world examples

  • Search-as-you-type, autosave drafts, window resize handlers, scroll-driven analytics.

Senior engineer discussion

Seniors structure debounce + cancel/flush + AbortSignal and pair with useMemo or refs in React. They also know lodash exists and use it instead in production where appropriate.

Related questions