JavaScript Interview Prep
Functions Deep Dive

Debouncing & Throttling

Rate-Limiting Your Event Handlers

LinkedIn Hook

Your search bar fires an API call on every single keystroke.

Your scroll handler runs 200 times in 2 seconds.

Your "Submit Order" button gets double-clicked and charges the card twice.

All three problems collapse into one answer: rate limiting. And JavaScript gives you two tools for it — debounce and throttle.

They look similar. They are not. Debounce waits for silence. Throttle enforces a speed limit. Pick the wrong one and your search bar never fires, or your scroll handler melts the CPU.

In this lesson I implement both from scratch — the bare version, then a production-grade variant with leading/trailing edges, cancel, and flush. The kind you'd ship in a real app, or write on a whiteboard in an interview.

If you've ever been asked "implement debounce from scratch" — this is the full answer.

Read the full lesson -> [link]

#JavaScript #InterviewPrep #Debounce #Throttle #Frontend #Performance #WebDevelopment


Debouncing & Throttling thumbnail


What You'll Learn

  • What debouncing and throttling actually do and when to reach for each
  • How to implement debounce from scratch — basic, then with leading/trailing/cancel/flush
  • How to implement throttle from scratch — basic, then with trailing call and cancel
  • The mental model to pick the right tool for search, scroll, submit, and resize events

The Elevator Analogy

Imagine you're in an elevator. Two different strategies for handling people pressing the "close door" button:

  • Debounce: The elevator waits. Every time someone presses the button, the timer resets. The door only closes once everyone has stopped pressing for 3 seconds.
  • Throttle: The elevator closes the door at most once every 3 seconds, regardless of how many times the button is pressed.

Both are rate-limiting techniques, but they work very differently.


What is Debouncing?

Debouncing delays the execution of a function until after a certain period of inactivity. If the function is called again before the delay expires, the timer resets.

Use cases:

  • Search input (wait until user stops typing)
  • Window resize handler
  • Auto-save in text editors
  • Form validation on input
// The problem: API call on every keystroke
searchInput.addEventListener("input", (e) => {
  fetchResults(e.target.value); // fires on EVERY keystroke!
});

// User types "javascript" = 10 API calls!
// With debounce = 1 API call (after user stops typing)

Implement Debounce from Scratch

This is one of the most commonly asked interview questions. Let's build it step by step.

// Basic debounce implementation
function debounce(fn, delay) {
  let timeoutId;

  return function (...args) {
    // Clear the previous timer
    clearTimeout(timeoutId);

    // Set a new timer
    timeoutId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// Usage
const debouncedSearch = debounce((query) => {
  console.log("Searching for:", query);
  // fetch(`/api/search?q=${query}`)
}, 300);

searchInput.addEventListener("input", (e) => {
  debouncedSearch(e.target.value);
});

Key details:

  • fn.apply(this, args) preserves the correct this context
  • clearTimeout cancels the previous pending call
  • Each new call resets the timer

Now let's add cancel functionality:

// Debounce with cancel
function debounce(fn, delay) {
  let timeoutId;

  function debounced(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  }

  debounced.cancel = function () {
    clearTimeout(timeoutId);
    timeoutId = null;
  };

  return debounced;
}

// Usage — cancel on component unmount
const debouncedSave = debounce(saveData, 1000);
// ... later, when component unmounts:
debouncedSave.cancel();

Debounce with Leading & Trailing Options

In real-world libraries (like Lodash), debounce supports two modes:

  • Trailing (default): Fires at the END of the delay
  • Leading: Fires IMMEDIATELY on the first call, then ignores subsequent calls within the delay
// Full debounce with leading/trailing options
function debounce(fn, delay, options = {}) {
  let timeoutId;
  let lastArgs;
  let lastThis;
  const leading = options.leading ?? false;
  const trailing = options.trailing ?? true;

  function debounced(...args) {
    lastArgs = args;
    lastThis = this;
    const isFirstCall = !timeoutId;

    clearTimeout(timeoutId);

    if (leading && isFirstCall) {
      fn.apply(lastThis, lastArgs);
    }

    timeoutId = setTimeout(() => {
      // Only fire trailing if it wasn't already fired as leading
      if (trailing && !(leading && isFirstCall)) {
        fn.apply(lastThis, lastArgs);
      }
      timeoutId = null;
      lastArgs = null;
      lastThis = null;
    }, delay);
  }

  debounced.cancel = function () {
    clearTimeout(timeoutId);
    timeoutId = null;
    lastArgs = null;
    lastThis = null;
  };

  debounced.flush = function () {
    if (timeoutId) {
      fn.apply(lastThis, lastArgs);
      debounced.cancel();
    }
  };

  return debounced;
}

// Leading: fire immediately, then wait
const onClickLeading = debounce(submitForm, 2000, { leading: true, trailing: false });
// Prevents double-click submissions!

// Trailing (default): wait, then fire
const onInputTrailing = debounce(search, 300, { trailing: true });
// Standard search-as-you-type pattern

What is Throttling?

Throttling ensures a function runs at most once within a specified time interval. Unlike debounce, continuous calls don't reset the timer.

Use cases:

  • Scroll event handlers (infinite scroll, parallax)
  • Mouse move tracking (drag and drop)
  • Game loop frame updates
  • Rate limiting API calls
  • Window resize calculations
// The problem: scroll fires hundreds of times
window.addEventListener("scroll", () => {
  calculateScrollPosition(); // runs 100+ times per second!
});

// With throttle: runs at most once every 100ms

Implement Throttle from Scratch

// Basic throttle implementation
function throttle(fn, interval) {
  let lastTime = 0;

  return function (...args) {
    const now = Date.now();

    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

// Usage
const throttledScroll = throttle(() => {
  console.log("Scroll position:", window.scrollY);
}, 100);

window.addEventListener("scroll", throttledScroll);

Now a more complete version with trailing call support:

// Throttle with trailing call and cancel
function throttle(fn, interval) {
  let lastTime = 0;
  let timeoutId = null;

  function throttled(...args) {
    const now = Date.now();
    const remaining = interval - (now - lastTime);

    if (remaining <= 0) {
      // Enough time has passed — execute immediately
      clearTimeout(timeoutId);
      timeoutId = null;
      lastTime = now;
      fn.apply(this, args);
    } else if (!timeoutId) {
      // Schedule a trailing call
      timeoutId = setTimeout(() => {
        lastTime = Date.now();
        timeoutId = null;
        fn.apply(this, args);
      }, remaining);
    }
  }

  throttled.cancel = function () {
    clearTimeout(timeoutId);
    timeoutId = null;
    lastTime = 0;
  };

  return throttled;
}

Debounce vs Throttle — Side by Side

Let's see both applied to the same scroll event to really understand the difference:

let debounceCount = 0;
let throttleCount = 0;
let rawCount = 0;

const debouncedHandler = debounce(() => {
  debounceCount++;
  console.log(`Debounce fired: ${debounceCount} times`);
}, 200);

const throttledHandler = throttle(() => {
  throttleCount++;
  console.log(`Throttle fired: ${throttleCount} times`);
}, 200);

window.addEventListener("scroll", () => {
  rawCount++;

  debouncedHandler();
  throttledHandler();

  // After scrolling continuously for 2 seconds then stopping:
  // rawCount:      ~120 (fires every ~16ms)
  // throttleCount: ~10  (fires every 200ms during scroll)
  // debounceCount: 1    (fires once, 200ms after scroll stops)
});

Debouncing & Throttling visual 1

The mental model:

FeatureDebounceThrottle
When it firesAfter inactivityAt regular intervals
Continuous inputKeeps delayingFires periodically
Best forFinal value (search)Ongoing updates (scroll)
AnalogyElevator door waitSpeed limit on a highway

Common Mistakes

  • Forgetting fn.apply(this, args) inside the setTimeout callback — you lose both the original this and the arguments from the most recent call, so event handlers silently break.
  • Using debounce on a "Submit Order" button with default trailing mode — the user clicks once, waits, wonders if anything happened, clicks again, and now the trailing timer fires with the wrong context. Use { leading: true, trailing: false } for that case.
  • Reaching for throttle on a search input because "scroll uses throttle, so input should too" — debounce is correct for search because you want the FINAL value after typing stops, not periodic snapshots mid-word.

Interview Questions

Q: Implement a debounce function from scratch.

function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

This is the #1 most common implementation question for functions.

Q: What happens if you debounce with delay 0?

It still defers execution to the next macrotask (via setTimeout(..., 0)). The function won't execute synchronously — it will run after the current call stack clears. This is useful for batching multiple synchronous calls into one.

Q: You have a "Submit Order" button. Which do you use — debounce or throttle?

Leading-edge debounce ({ leading: true, trailing: false }). You want the FIRST click to go through immediately, then ignore subsequent clicks for a period. Throttle would also work but leading debounce is more semantically correct — you want exactly one execution triggered by the first event.

Q: Can debounce cause a function to never execute?

Yes. If the event keeps firing continuously without any pause longer than the delay, the debounced function will never execute (the timer keeps getting reset). This is why throttle is better for continuous events like scroll — it guarantees periodic execution.

Q: Debounce vs throttle in one sentence?

Debounce waits for silence; throttle enforces a speed limit.


Quick Reference — Cheat Sheet

DEBOUNCE vs THROTTLE — QUICK MAP

Events:   |||||||||||----------|||||||||||---------
Debounce: ................X...............X.......
Throttle: |....|....|....|....|....|....|....|....

Debounce: "Wait until they stop, then fire once"
Throttle: "Fire at most once per interval"

+------------------+------------------+------------------+
|    Debounce      |     Throttle     |    Neither       |
+------------------+------------------+------------------+
| Search input     | Scroll handler   | Click -> navigate|
| Resize calc      | Mouse move       | Form submit      |
| Auto-save        | Game loop        | Toggle on/off    |
| API suggestions  | API rate limit   | Modal open/close |
+------------------+------------------+------------------+

Debounce options:
  leading:  true  -> fire on first call, ignore until silence
  trailing: true  -> fire after silence (default)
  .cancel()       -> drop any pending call
  .flush()        -> run pending call now

Throttle options:
  trailing call   -> schedule one last run after interval
  .cancel()       -> reset internal timer

Previous: Function Composition -> Assembly-Line Functions Next: Memoization -> The Cache-Your-Answers Pattern


This is Lesson 6.7 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.

On this page