Suggest an editImprove this articleRefine the answer for “Debounce and throttle in JavaScript”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Debounce** postpones a function until after a pause in calls. **Throttle** limits it to at most once per time interval. ```javascript // Debounce: one API call 300ms after typing stops const debounced = debounce(search, 300); // Throttle: scroll handler fires max once per 200ms const throttled = throttle(onScroll, 200); ``` **Key rule:** user stops then acts - debounce. Steady rate during activity - throttle.Shown above the full answer for quick recall.Answer (EN)Image**Debounce** postpones a function until after a pause in calls. **Throttle** limits it to at most once per time interval. ## Theory ### TL;DR - Debounce waits for silence: fires once after calls stop, not during them - Throttle keeps a beat: fires at a fixed rate during continuous activity - Analogy: debounce is a restroom that locks for 5 minutes after each use; throttle is a coffee refill available every 5 minutes max - User stops then acts? Debounce. Need a steady rate? Throttle. ### Quick example ```javascript // Debounce: fires once after calls stop const debounce = (fn, delay) => { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; }; // Throttle: fires at most once per delay const throttle = (fn, delay) => { let lastCall = 0; return (...args) => { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; fn(...args); } }; }; ``` Both are closures. `debounce` resets a timer on every call. `throttle` checks a timestamp and drops calls that arrive too soon. ### Key difference Debounce collects a burst of calls and fires exactly once, after the burst ends. That last word is the whole point. Throttle does not wait for an end. It fires at the start, then once more per interval, for as long as calls keep coming. ### When to use - User typing in a search box → **debounce** (one API call after they pause, not per keystroke) - Window scroll or resize → **throttle** (update layout at a steady pace) - Form submit button → **throttle** (block duplicate submissions) - External API with rate limits → **throttle** (space requests evenly) - Field validation while typing → **debounce** (check after pause, not mid-word) ### Comparison table | Aspect | Debounce | Throttle | |--------|----------|---------| | **When it fires** | After last call + delay | Immediately, then once per interval | | **During rapid calls** | Resets each time, fires once at end | Fires at fixed intervals | | **Total executions per burst** | Usually 1 | Multiple, rate-limited | | **Default behavior** | Trailing (after silence) | Leading (first call goes through) | | **Best for** | Autocomplete, auto-save, validation | Scroll, mousemove, API polling | ### How it works internally `debounce` calls `clearTimeout` on every invocation. That cancels the pending callback. Only the last call survives, because nothing cancels it. `throttle` skips the timer entirely in its basic form. It uses `Date.now()` to compare the current time against the last execution. If the gap is smaller than the delay, the call is dropped. One thing I got wrong early on: throttle fires immediately on the first call, because `lastCall` starts at 0, so `Date.now() - 0` is always larger than any realistic delay. The first call always goes through. Both patterns rely on the browser event loop. Timer callbacks run in the macrotask queue, after the current synchronous code finishes. ### Common mistakes **Mistake 1: No cleanup on unmount in React** ```javascript // Wrong: timer fires after component is gone useEffect(() => { debouncedFetch(query); }, [query]); // Correct: cancel on cleanup useEffect(() => { const timer = setTimeout(() => fetchData(query), 300); return () => clearTimeout(timer); }, [query]); ``` Without cleanup, the timer calls `setState` on an unmounted component. That throws warnings in dev mode and can break server-side rendering. **Mistake 2: New debounced function on every render** ```javascript // Wrong: new function each render means the timer resets constantly function SearchInput() { const handleChange = debounce((val) => search(val), 500); return <input onChange={handleChange} />; } // Correct: create once function SearchInput() { const handleChange = useMemo( () => debounce((val) => search(val), 500), [] ); return <input onChange={handleChange} />; } ``` **Mistake 3: Expecting throttle to skip the first call** ```javascript const throttled = throttle(fn, 1000); throttled(); // Fires immediately (leading edge) throttled(); // Skipped throttled(); // Skipped ``` Lodash `_.throttle` fires on the leading edge by default. Pass `{ leading: false }` to change this. **Mistake 4: Losing `this` context** ```javascript // Wrong: `this` is lost inside the wrapper function debounce(fn, delay) { let timer; return function() { clearTimeout(timer); timer = setTimeout(fn, delay); // `this` not forwarded }; } // Correct function debounce(fn, delay) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); // ✅ }; } ``` **Mistake 5: Nesting debounce inside throttle** ```javascript // Broken: inner debounce resets every time throttle fires const broken = throttle(() => debounce(save, 500)(), 1000); // save is never called ``` One wrapper is enough. Nesting them causes the inner timer to reset on every outer call. ### Real-world usage - React search bars use the `use-debounce` hook (same pattern as Vercel commerce templates) - Lodash `_.debounce` and `_.throttle` appear across most large npm projects for scroll and input handling - `express-rate-limit` in Node.js/Express follows the throttle pattern for API endpoints - For scroll and resize at 60fps, `requestAnimationFrame` is often a better fit than a fixed-delay throttle - Native `ResizeObserver` removes the need for manual throttling of resize events in modern browsers ### Follow-up questions **Q:** Implement debounce from scratch. **A:** Use `clearTimeout` and `setTimeout` inside a closure. Preserve `this` and args with `.apply(this, args)`. The Quick example above shows the minimal version. **Q:** What happens when `delay` is 0? **A:** Debounce fires on the next event loop tick after each call. Throttle passes through every call, because `Date.now() - 0` is always at least 0. **Q:** How do you fix a memory leak from debounce in React? **A:** Return a cleanup function from `useEffect` that calls `clearTimeout`. Store the timer ID in a ref with `useRef` so it persists across renders. **Q:** What is the difference between leading and trailing edge in throttle? **A:** Leading fires on the first call immediately. Trailing fires at the end of the interval with the last set of arguments. Lodash `_.throttle` does both by default, which causes an extra call at the end of a burst that surprises most people. **Q:** How do you debounce an async function? **A:** The wrapper returns `void`, not a Promise. To get the resolved value, pass it through a callback, or track the latest Promise in a ref and resolve it separately. Multiple in-flight promises can race if you are not careful. ## Examples ### Basic: debounce for a search input ```javascript const debounce = (fn, delay) => { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; }; const input = document.querySelector('#search'); const search = debounce((event) => { console.log('API call with:', event.target.value); // fetch(`/api/search?q=${event.target.value}`) }, 400); input.addEventListener('input', search); // Typing 'hello' triggers 5 input events // Result: 1 API call, 400ms after the last keystroke ``` Without debounce, typing five characters produces five API calls. With it, one call arrives after the user pauses. ### Intermediate: throttled scroll handler for infinite scroll ```javascript const throttle = (fn, delay) => { let lastCall = 0; return (...args) => { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; fn(...args); } }; }; const handleScroll = throttle(() => { const nearBottom = window.scrollY + window.innerHeight >= document.body.scrollHeight - 100; if (nearBottom) { loadMoreItems(); } }, 200); window.addEventListener('scroll', handleScroll); // Max 5 checks per second, regardless of scroll speed ``` A fast scroll without throttle fires hundreds of events per second. At 200ms you get five checks. That is enough for infinite scroll to feel instant without hammering the browser. ### Advanced: React custom hook with proper cleanup ```javascript import { useCallback, useRef, useEffect } from 'react'; function useDebounce(callback, delay) { const timerRef = useRef(null); useEffect(() => { return () => clearTimeout(timerRef.current); }, []); return useCallback((...args) => { clearTimeout(timerRef.current); timerRef.current = setTimeout(() => callback(...args), delay); }, [callback, delay]); } function SearchBox() { const handleSearch = useDebounce((value) => { fetch(`/api/search?q=${value}`).then((res) => res.json()); }, 300); return ( <input type='text' onChange={(e) => handleSearch(e.target.value)} placeholder='Search...' /> ); } // useEffect cleanup cancels the timer on unmount // No setState calls after the component is gone ``` The `useEffect` cleanup is the piece most hand-rolled implementations skip. Without it, a user who navigates away mid-typing triggers a fetch that tries to update state on an unmounted component.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.