Debounce & Throttle โ€” implement from scratch

Two patterns every frontend engineer must be able to implement cold: what they do, when each fits, and the code.

must medium โฑ 20 min debouncethrottleperformanceclosurestimers
Mastery:
Why interviewers ask this
Implementing debounce is one of the top-5 most common JS coding questions. It tests closures, timers, and practical knowledge in one small function. Throttle usually follows as a variant.

Debounce delays execution until a pause in events โ€” fire only after the user stops triggering events for X ms. Throttle limits rate โ€” fire at most once every X ms regardless of how fast events arrive.

A mnemonic: debounce = elevator door (keeps reopening as people walk in; only closes after no one enters for a moment). Throttle = shooting a machine gun (fires at a fixed rate no matter how fast you pull the trigger).

Debounce โ€” implementation

The key insight: every call clears the previous timer and starts a fresh one. Only when the timer actually fires does the wrapped function run.

Run it yourself
Edit and run. Output is captured from console.log. Async logs (setTimeout/Promise) appear in real execution order.
Console
 

Real uses: search-box input (wait for user to stop typing before fetching), window resize handler, form auto-save.

Debounce with leading edge

Sometimes you want to fire immediately on the first call, then silence until the burst ends:

function debounce(fn, ms, { leading = false } = {}) {
  let timerId;
  let hasLeadFired = false;

  return function (...args) {
    if (leading && !timerId) {
      fn.apply(this, args);     // fire immediately on first call
      hasLeadFired = true;
    }
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      timerId = null;
      hasLeadFired = false;
      if (!leading) fn.apply(this, args);
    }, ms);
  };
}

Debounce with cancel

A .cancel() method is often asked for as a follow-up:

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

Throttle โ€” implementation

Record when the function last ran. On each call, check if enough time has passed; if yes, run it and update the timestamp.

Run it yourself
Edit and run. Output is captured from console.log. Async logs (setTimeout/Promise) appear in real execution order.
Console
 

Real uses: scroll event handlers, mouse-move tracking, game input loops, rate-limiting API calls.

Throttle with trailing call (timer variant)

The timestamp approach drops trailing calls. If you want the last call in a burst to always fire, combine a timestamp with a timer:

function throttle(fn, ms) {
  let lastTime = 0;
  let timerId;
  return function (...args) {
    const now = Date.now();
    const remaining = ms - (now - lastTime);
    clearTimeout(timerId);
    if (remaining <= 0) {
      lastTime = now;
      fn.apply(this, args);
    } else {
      timerId = setTimeout(() => {
        lastTime = Date.now();
        fn.apply(this, args);
      }, remaining);
    }
  };
}

When to reach for each

SituationUse
Search box โ€” fetch after user stops typingDebounce
Window resize โ€” recalculate layoutDebounce
Scroll position trackingThrottle
Mouse-move tooltipThrottle
Button that must not double-fireDebounce (leading)
Game loop / animation frame rate capThrottle

Say it out loud
โ€œDebounce delays execution until events stop โ€” each call resets a timer; only when the timer fires does the function run. Throttle limits frequency โ€” it checks if enough time has passed since the last call. Both are closures over a timer or timestamp. I use debounce for search inputs and form auto-save, throttle for scroll and mouse-move handlers.โ€

Likely follow-up questions
  • What's the difference between debounce and throttle?
  • How do you implement a leading-edge debounce (fire immediately, then wait)?
  • How would you add a cancel method to your debounce?
  • Where in a real app would you use each?
  • How does lodash's debounce differ from a naive implementation?

References