Lesson 76 of 80
"Implement debounce" is the single most-asked live-coding question in frontend interviews. Here's both debounce and throttle built line by line, with every line explained.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Knowing what debounce and throttle do is one thing. Being handed a blank editor and asked to write one, live, while someone watches, is another. This lesson is the second one — building both from nothing, explaining why each line exists, not just what it does.
The two functions are short — eight lines each — which is exactly why interviewers like them. There's nowhere to hide a gap in understanding.
function debounce(fn, delay) {
let timerId; // 1. private state, one per wrapped fn
return function debounced(...args) { // 2. the function callers actually call
const context = this; // 3. preserve `this` for method calls
clearTimeout(timerId); // 4. cancel whatever was pending
timerId = setTimeout(() => { // 5. schedule a new attempt
fn.apply(context, args); // 6. run the real function, with its args
}, delay);
};
}
Walk it as an interviewer would grill it:
timerId has to live outside the returned function, or every call would start from scratch with nothing to cancel. This is the closure doing its job.fn is a method (obj.save = debounce(obj.save, 300)), losing this breaks it silently. Capturing context and using .apply is what saves you here — an arrow function for the outer wrapper would also lose the caller's this, which is why debounced is a regular function.delay survives....args and .apply(context, args) together forward whatever the caller passed, unchanged.Now throttle, which is a different shape because it doesn't cancel anything — it gates:
function throttle(fn, interval) {
let isWaiting = false; // 1. are we inside a "cooldown" window?
return function throttled(...args) {
if (isWaiting) return; // 2. drop calls made during cooldown
fn.apply(this, args); // 3. run immediately on the leading edge
isWaiting = true; // 4. start the cooldown
setTimeout(() => {
isWaiting = false; // 5. cooldown over, next call can run
}, interval);
};
}
The key difference from debounce: throttle runs fn immediately on the first call, then ignores everything until the cooldown ends. Debounce never runs immediately — it always waits for quiet first.
this. A debounce/throttle that only forwards args and drops this works fine on standalone functions but silently breaks when wrapping an object method — a common interview trap planted on purpose.fn might return something. A production debounce usually can't return it synchronously (the call is deferred), which is worth saying out loud in an interview even if you don't solve it — it shows you understand the async nature of what you just wrote.This track already covers debounce and throttle as a concept — what problem each one solves and when to reach for which. This lesson is that same idea taken one level deeper: not recognizing debounce and throttle, but being able to produce a correct implementation of either one from an empty editor, under time pressure, and defend every line of it out loud.