How do you implement debouncing and throttling in JavaScript?

Advanced15 min interview
Skills tested:
Implementing debounce from scratch using setTimeout and clearTimeoutImplementing throttle from scratch using a flag and setTimeoutChoosing between debounce and throttle for a given event typeUnderstanding closure usage in both implementationsEdge cases: leading vs trailing invocation, cancellation

Advertisement

🧩 Scenario

In a real codebase, debounce and throttle are essential for controlling the rate of expensive operations triggered by high-frequency events. Search inputs that fire API calls on every keystroke, scroll handlers that recompute layout, resize listeners that recalculate responsive breakpoints, and window mousemove handlers all need rate limiting to avoid performance problems. Both patterns use closures to maintain state across calls and higher-order functions to wrap the original behavior.

Architecture Walkthrough

Debounce: Wait for the Noise to Stop

Debounce delays the execution of a function until after a specified period of inactivity. Every time the debounced function is called, any pending timer is cleared and a new one is started. The wrapped function only executes when the timer completes without being interrupted. If events arrive faster than the delay, the function is never called until they stop.

The implementation uses a closure to maintain the timeout reference across calls. clearTimeout cancels the previous timer, and setTimeout schedules a new one. This pattern is ideal for any event where you only care about the final state after a burst of activity ends: search input, window resize completion, form validation after typing stops.

Throttle: Limit the Rate

Throttle ensures a function is called at most once per interval, regardless of how many times it is invoked. The first call in each interval always executes immediately. Subsequent calls during the same interval are ignored. After the interval passes, the flag resets and the next call can execute.

The implementation uses a boolean flag to track whether the function is currently in its quiet period. The flag is set on the first call, and a timer resets it after the interval. Throttle is appropriate when you want regular, periodic execution during continuous activity: scroll position updates, rate-limited analytics, and canvas mouse-tracking all benefit from throttle rather than debounce.

Closures Power Both Patterns

Both debounce and throttle are higher-order functions that return a new function. The returned function closes over the timeout or inThrottle variable from the outer scope. This closure is what allows the timer state to persist between separate calls to the returned function without using global variables.

In React, this means the debounced or throttled function must be stable across renders. Creating it inside the component body without useCallback produces a new function on every render, discarding the timer state. The useCallback hook (or a useRef to store the function) is necessary to maintain the closure correctly.


Key Code Explained

// Debounce: resets timer on every call
function debounce(fn, delay) {
  let timeout;
  return function (...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => fn.apply(this, args), delay);
  };
}

// Throttle: executes at most once per interval
function throttle(fn, limit) {
  let inThrottle = false;
  return function (...args) {
    if (!inThrottle) {
      fn.apply(this, args);
      inThrottle = true;
      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  };
}

// Debounce in React: memoize with useCallback to preserve closure
const handleSearch = useCallback(
  debounce((query) => {
    fetchResults(query);
  }, 300),
  [], // stable reference across renders
);

// Cleanup in useEffect to avoid memory leaks
useEffect(() => {
  const debouncedResize = debounce(handleResize, 200);
  window.addEventListener('resize', debouncedResize);
  return () => {
    window.removeEventListener('resize', debouncedResize);
    // Also clear any pending timer if debounce exposes a cancel method
  };
}, []);

The React usage pattern is the most important real-world detail. Without useCallback, every render creates a new debounced function with a fresh timer, so the accumulated delay resets every time the component re-renders. Wrapping with useCallback and an empty dependency array ensures the same debounced function instance is reused.


Tradeoffs

PatternFires whenRisk of missing eventsBest for
Debounce (trailing)After activity stopsYes, intermediate events are lostSearch input, form validation, resize completion
Debounce (leading)On first call, then after quiet periodYes, intermediate events are lostPreventing double-submit on a button
ThrottleAt most once per intervalYes, but at a predictable rateScroll, mousemove, real-time analytics
No rate limitingOn every eventNoSimple, infrequent events only

What Interviewers Actually Check

  • Whether you can implement both from scratch without reaching for a library
  • Whether you understand the closure mechanics that make the timer state persist
  • Whether you can explain when each pattern is appropriate and give concrete examples
  • Whether you know the React-specific pitfall of recreating debounced functions on every render
  • Whether you can describe leading vs trailing invocation

Follow-Up Questions

  1. How would you add a cancel method to a debounce implementation that clears the pending timer?
  2. How does requestAnimationFrame relate to throttling for animation and rendering use cases?
  3. If both debounce and throttle cause you to miss events, what pattern would you use when you need the last event's value but want to rate-limit the frequency?
  4. How would you test a debounced function in a unit test without actually waiting for the timer?
  5. Lodash provides _.debounce and _.throttle with leading, trailing, and maxWait options. When would you need maxWait?

Common Candidate Mistakes

  • Using debounce for scroll events and losing the intermediate scroll positions that the handler needed to process
  • Using throttle for search input and sending unnecessary API calls every interval instead of waiting for the user to pause
  • Not using useCallback in React for debounced handlers, causing timer state to reset on every render
  • Creating the debounced function inside a useEffect or callback where it is recreated on every invocation
  • Not cleaning up the event listener and timer on component unmount

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you write a debounce implementation from scratch and explain how clearTimeout resets the delay?
  • Can you write a throttle implementation from scratch and explain the flag and timer mechanism?
  • Can you explain the difference between leading and trailing invocation for both patterns?
  • Can you describe how to properly use a debounced function in React with useCallback and cleanup?
  • Can you name three real event types that benefit from debounce and three that benefit from throttle?

Summary

Debounce and throttle are rate-limiting patterns for high-frequency events. Debounce delays execution until activity has paused for a specified duration: every new event resets the timer, and the function only runs after quiet. Throttle limits execution to at most once per interval: the first call runs, then the function is blocked until the interval resets.

Both patterns are higher-order functions that use closures to maintain timer state across calls. In React, the debounced or throttled function must be stable across renders, which requires useCallback or a ref to hold the instance. Creating it inside the render body defeats the purpose by discarding the accumulated timer state on every render.

Choosing between them depends on the event type: debounce is for situations where only the final state matters (typing is done, resizing is complete), and throttle is for situations where periodic updates during activity are valuable (scroll position, mouse coordinates, live analytics).

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

When should I use debounce vs throttle?

Use debounce for actions that should fire once after activity stops, like search input. Use throttle for continuous events that should fire at a controlled rate, like scroll or resize.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement