Debounce and throttle
Control high-frequency work with correct leading and trailing behavior.

WHAT YOU WILL BE ABLE TO DO
Learning outcomes
- Explain debounce and throttle in plain language.
- Connect the behavior to the underlying browser or framework model.
- Implement the core pattern and reason through edge cases.
- Answer common follow-ups without relying on memorized phrases.
01Use cases
Debounce collapses a burst of calls into one, firing only after the calls stop for a set delay — ideal for search-as-you-type or resize handlers where only the final state matters.
Throttle guarantees a maximum call rate, firing at most once per interval regardless of how many calls arrive — ideal for scroll-position tracking or drag handlers where you need steady updates, not just the last one.
02Implementations
function debounce(fn, delay) { let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); };}function throttle(fn, interval) { let last = 0; return function (...args) { const now = Date.now(); if (now - last >= interval) { last = now; fn.apply(this, args); } };}03Edge cases
- Clear pending timers on unmount so a debounced call doesn't fire against a removed component.
- Decide leading vs. trailing explicitly — a search box usually wants trailing only; a save-indicator may want leading.
- Watch this binding when passing a method reference directly as a handler; apply/bind preserves it.
- Throttle by time, not by frame, if the handler must run during a drag on lower frame rates.
DDConcept deep dives
Deep dive 1
Debounce models a quiet-period decision
Every call resets a waiting period. Only after activity stops does the trailing invocation run with the latest arguments. That semantics fits operations where intermediate values are obsolete, such as a search request after typing pauses. A leading option changes the experience by responding immediately, but the utility must define what happens to a later trailing call.
- Debounce the expensive consequence, not necessarily the visible input update.
- The delay expresses a product trade-off between responsiveness and wasted work.
- A maximum wait may be needed when activity can continue indefinitely.
Deep dive 2
Throttle models a rate limit
A throttle allows execution no more frequently than a chosen interval while calls continue. It is appropriate when intermediate observations still matter but processing every event is excessive. For visual DOM work, animation-frame scheduling often provides a more meaningful cadence because it aligns one update with the browser's rendering opportunity.
- Keep the most recent arguments when a trailing result should reflect current state.
- Time throttling and frame coalescing have different behavior in background tabs.
- Passive listeners improve scrolling only when the handler does not need to cancel the event.
Deep dive 3
Lifecycle and race control are separate
A production wrapper needs cancel and sometimes flush behavior so delayed work cannot outlive its owner. Debouncing a fetch does not cancel a request already sent and does not guarantee response order. Treat timer ownership, network cancellation, and stale-result protection as separate concerns even though they cooperate in one feature.
- Cancel timers during unmount or when the pending action becomes irrelevant.
- Use AbortController for supported operations and still guard authoritative result identity.
- Preserve this and argument types when building a reusable utility.
QAInterview questions and model answers
Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.
Intermediate · Coding · 1 min · Question 1How do debounce and throttle differ?Open model answer
Model answer
Debounce waits for a quiet period before invoking, so a burst collapses into one call. Throttle limits invocation to at most once per interval while activity continues. Search suggestions often debounce; continuous scroll measurement may throttle or align work to animation frames.
function debounce(fn, delay) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), delay); // fires only after calls stop };}
function throttle(fn, interval) { let last = 0; return (...args) => { const now = Date.now(); if (now - last >= interval) { last = now; fn(...args); } // at most 1/interval };}Intermediate · Conceptual · 1 min · Question 2What do leading and trailing options mean?Open model answer
Model answer
Leading invokes at the start of the window; trailing invokes with the latest arguments after the window. Enabling both requires careful state so a single call does not unexpectedly fire twice. The product interaction should determine the semantics.
Open question page →Intermediate · Coding · 1 min · Question 3Why should a debounce utility expose cancel and flush?Open model answer
Model answer
Cancel prevents obsolete work when a component unmounts or input loses relevance. Flush immediately runs pending trailing work, useful before form submission or navigation. Without lifecycle controls, delayed callbacks can update stale UI.
function debounce(fn, delay) { let t, lastArgs; const debounced = (...args) => { lastArgs = args; clearTimeout(t); t = setTimeout(() => fn(...lastArgs), delay); }; debounced.cancel = () => clearTimeout(t); debounced.flush = () => { clearTimeout(t); if (lastArgs) fn(...lastArgs); }; return debounced;}Intermediate · Coding · 1 min · Question 4What values must a robust wrapper preserve?Open model answer
Model answer
It should preserve the caller's this value and latest arguments, return semantics where meaningful, and clearly define timing behavior. TypeScript utilities should also preserve the original function's parameter types.
function throttle(fn, interval) { let last = 0; return function (...args) { const now = Date.now(); if (now - last >= interval) { last = now; return fn.apply(this, args); // keep `this` and pass args through } };}Intermediate · Coding · 1 min · Question 5Is requestAnimationFrame a throttle?Open model answer
Model answer
It can coalesce visual work to at most once per rendering frame, which is often better than an arbitrary millisecond interval for DOM reads and writes. It is not a general time-based throttle and pauses or slows in background tabs.
function rafThrottle(fn) { let scheduled = false; return (...args) => { if (scheduled) return; scheduled = true; requestAnimationFrame(() => { scheduled = false; fn(...args); }); };}Beginner · Conceptual · 1 min · Question 6Should every input handler be debounced?Open model answer
Model answer
No. Local controlled-input state should normally update immediately. Debounce expensive downstream effects such as network requests, validation, or filtering, and ensure assistive feedback is not delayed unreasonably.
Open question page →Advanced · Conceptual · 1 min · Question 7How do you type a debounce function in TypeScript?Open model answer
Model answer
Make it generic over the original function's parameter tuple, preserve those parameters in the wrapper, and explicitly type cancel or flush methods and return semantics.
Open question page →Advanced · Conceptual · 1 min · Question 8What should happen if a debounced callback throws?Open model answer
Model answer
Synchronous leading or flushed invocation can propagate normally, while delayed invocation has no original caller to receive it. The API must document error handling rather than silently swallowing failures.
Open question page →Intermediate · Conceptual · 1 min · Question 9How do you test timing utilities reliably?Open model answer
Model answer
Use a controlled clock, advance time around exact boundaries, and assert leading, trailing, cancellation, latest arguments, and repeated-call behavior without real sleeps.
Open question page →Beginner · Conceptual · 1 min · Question 10When is server-side rate limiting still required?Open model answer
Model answer
Always when protecting a service or enforcing quotas. Client throttling improves experience and traffic shape but can be removed or bypassed by any caller.
Open question page →SCScenario questions
Scenario 1
A debounced autocomplete shows results for an older query after a newer request finishes. Fix the design.
- Separate invocation control from request race control.
- Abort the previous request when a new query becomes authoritative.
- Associate responses with a request id or query snapshot.
- Ignore stale results even if cancellation is unavailable.
Reveal worked answer
Debouncing reduces request frequency but does not guarantee response order. I would abort the previous fetch with AbortController and also compare a monotonically increasing request id before committing results. Cleanup cancels the timer and request so an unmounted component cannot be updated.