function debounce(fn, delay = 300) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// Only fires 200ms after the user stops resizing the window.
const onResize = debounce(() => console.log("resized"), 200);
window.addEventListener("resize", onResize);// Resize the window and stop:
resized // logged once, 200ms after the last resize eventOpen this snippet in the editor
Launches a fresh code space with this JavaScript already loaded — edit it, share the link, or keep building.
How it works
Debouncing collapses a burst of rapid events into a single call that runs once the burst is over. Each invocation clears the pending timer and schedules a new one, so the wrapped function only executes after `delay` milliseconds of silence. If the events keep arriving faster than the delay, the inner function never runs until they finally pause — which is usually exactly the behaviour you want.
The classic motivating example is a search box. Without debouncing, a handler bound to the input fires on every keystroke, so typing "javascript" sends ten requests when you only care about the last one. Wrapping the handler in `debounce` turns that into a single request after the user stops typing, cutting load on your server and avoiding a flood of out-of-order responses racing back to the UI.
Notice the wrapper is a regular `function`, not an arrow. That's deliberate: it preserves the `this` value the caller invokes it with and forwards it through `fn.apply(this, args)`. When the debounced function is used as a method or a DOM event listener — where `this` points at the element — that forwarding keeps the original behaviour intact. An arrow function would capture the surrounding `this` instead and quietly break those cases.
Debounce is one of two closely-related rate-limiting tools. Its sibling, throttle, runs the function at a fixed maximum rate *during* continuous activity rather than only at the end. Use throttle when you want steady updates while something is happening (a scroll-position indicator, a drag handler); use debounce when you only care about the final state once the activity settles (search input, window resize, autosave).
Variations
Leading edge — fire immediately, then ignore the burst
Sometimes you want the first call to run instantly (so the UI feels responsive) and the rest of the burst suppressed. Flip the timing so the function runs on the leading edge instead of the trailing one.
function debounceLeading(fn, delay = 300) {
let timer;
return function (...args) {
if (!timer) fn.apply(this, args);
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
}, delay);
};
}Cancellable + flush
For real components you often need to cancel a pending call (e.g. on unmount) or force it to run now. Expose `cancel` and `flush` on the returned function.
function debounce(fn, delay = 300) {
let timer;
let pending;
function debounced(...args) {
pending = () => fn.apply(this, args);
clearTimeout(timer);
timer = setTimeout(() => {
pending = null;
fn.apply(this, args);
}, delay);
}
debounced.cancel = () => clearTimeout(timer);
debounced.flush = () => {
clearTimeout(timer);
if (pending) pending();
};
return debounced;
}Search-as-you-type without hammering the API
Here's the whole point of debouncing in one small example. The input handler is debounced at 300ms, so the network request only goes out once the user pauses — not on every keystroke.
const search = debounce(async (query) => {
if (!query) return;
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const results = await res.json();
render(results);
}, 300);
input.addEventListener("input", (e) => search(e.target.value));Common mistakes & good to know
- Debounce is not throttle. Debounce waits for activity to stop; throttle runs at a fixed rate during activity. Use throttle for scroll-position readouts, debounce for the final value.
- Define the debounced function once (module scope, or `useMemo`/`useRef` in React). Recreating it on every render or every event gives each call its own timer and defeats the purpose entirely.
- Arrow functions have no own `this`. If your handler relies on `this`, keep the returned wrapper a regular function as shown.
- Debounced work that's still pending when a component unmounts can call into a dead component. Cancel it on cleanup (the cancellable variant above).
Frequently asked questions
What's the difference between debounce and throttle?
Debounce delays the call until the activity stops, so it runs once at the end. Throttle lets the call run at most once per interval while the activity continues. Search inputs want debounce; scroll or mousemove indicators usually want throttle.
How do I debounce in React?
Create the debounced function once with useMemo (or store it in a useRef) so it survives re-renders, and cancel it in a useEffect cleanup on unmount. Recreating it inline every render breaks the timer.
What delay should I use?
200–300ms is the usual sweet spot for typing and resize — long enough to absorb a burst, short enough to still feel instant. Tune it to the action: autosave can be 500–1000ms, a fast filter can be 150ms.
Can I cancel a pending debounced call?
Yes — expose a cancel method that calls clearTimeout, as in the cancellable variant. That's important for cleanup so a pending call doesn't fire after the relevant component or context is gone.
Related snippets
Previous
Binary search in Python
Next
Deep clone an object in JavaScript