Debounce and throttle are different tools for different problems
Debounce and throttle both reduce how often a function runs in response to a burst of events, and they are constantly confused because that one-line summary makes them sound interchangeable. They are not. They answer different questions. Debounce asks “has the user stopped?” and runs once after the burst goes quiet. Throttle asks “has enough time passed?” and runs at a steady maximum rate during the burst. Pick the wrong one and the feature feels broken in a way that is hard to name — a search box that fires on every keystroke, or a scroll handler that stutters.
Debounce: wait for the user to stop
Debounce delays the call until the events pause for a set interval. Every new event resets the timer, so nothing runs until the burst goes quiet. This is exactly right for a search-as-you-type box: you want to hit the API once the user finishes typing, not on every keystroke.
function debounce(fn, wait) {
let timer;
return (...args) => {
clearTimeout(timer); // each event cancels the pending call
timer = setTimeout(() => fn(...args), wait);
};
}
const search = debounce((q) => fetchResults(q), 300);
input.addEventListener("input", (e) => search(e.target.value));
// types "cats" fast → ONE fetch, 300ms after the last keystroke
Use debounce for search inputs, autosave-after-editing, resize handlers that recompute a layout, and validating a field once the user leaves it. The tell is “do the work once, when the activity settles.”
Throttle: enforce a steady maximum rate
Throttle runs the function at most once per interval during a continuous burst — it does not wait for quiet, it caps the rate. This is what you want for a scroll or mousemove handler: you need periodic updates while the burst is still happening, just not sixty of them a second.
function throttle(fn, interval) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= interval) { // enough time passed? run and mark
last = now;
fn(...args);
}
};
}
const onScroll = throttle(() => updateStickyHeader(), 100);
window.addEventListener("scroll", onScroll);
// scrolls continuously → the header updates ~10x/sec, smoothly, not per frame
Picking the wrong one is the bug
Debounce a scroll handler and the sticky header never moves while you scroll — it only snaps into place after you stop, because debounce is still waiting for quiet that a continuous scroll never gives it. Throttle a search box and you fire a request every 300ms during typing, hammering the API with queries for half-typed words. The question to ask is never “which is faster” — it is “do I want the result after the activity, or during it?” After the burst, debounce; throughout the burst, throttle. The debounce-utility exercise builds the timer-reset version above and is the fastest way to make the distinction stick in your hands rather than just your notes.