Stockio

Debounce vs Throttle in JavaScript (With Code)

Choose debounce or throttle for search, resize, and pointer events. Implement cancellable helpers, understand timing tradeoffs, and prevent stale asynchronous results.

By Daniel Kim

React Developer ·

On this page

A user can generate events much faster than an application should perform expensive work. Typing produces repeated input events, dragging produces pointer movement, and resizing can trigger continuous layout updates. Debounce and throttle control when a callback runs, but they solve different problems and carry different promises about responsiveness.

Choose a policy from the interaction

Debounce waits for a quiet period before running work. It suits a search box where intermediate input is usually not worth requesting. Throttle limits execution frequency while events continue. It suits periodic progress updates where waiting for complete silence would hide useful feedback.

Before choosing a delay, inspect the underlying operation. Use the API latency tester with an endpoint you control to understand its response behavior, subject to the tool's browser and network constraints. A debounce interval controls when a request starts; it cannot make a slow service finish sooner or reveal every user's network conditions.

For local interactions, test with realistic event streams instead of using network latency as the only guide. A text field should continue updating immediately even if its remote suggestions wait. The micro-interactions guide provides useful context for separating immediate feedback from deferred side effects.

Implement a trailing debounce with cancellation

A trailing debounce runs once after calls stop for the configured interval. Each new call clears the previous timer and captures the latest arguments. The helper below also preserves the receiver for ordinary functions and exposes cancellation for lifecycle cleanup.

function debounce(fn, wait = 250) {
if (!Number.isFinite(wait) || wait < 0) {
  throw new RangeError("wait must be a nonnegative finite number");
}
let timer;

function debounced(...args) {
  clearTimeout(timer);
  const receiver = this;
  timer = setTimeout(() => {
    timer = undefined;
    fn.apply(receiver, args);
  }, wait);
}

debounced.cancel = () => {
  clearTimeout(timer);
  timer = undefined;
};
return debounced;
}

const search = debounce(query => {
console.log("Search for", query);
}, 300);

search("ja");
search("javascript"); // Only this value runs after silence.

This wrapper intentionally returns no callback result. A delayed function cannot synchronously return a value that does not exist yet. Promise-returning debounce utilities need a documented policy for superseded calls, cancellation, and errors; adding async to the wrapper does not solve those questions automatically.

A trailing-only debounce can postpone execution indefinitely during continuous activity. Autosave may therefore need maximum wait behavior that periodically persists changes even while editing continues. This implementation does not provide that feature. Choose a maintained utility when requirements include maximum waits, flushing, or several leading and trailing combinations.

Timers specify a minimum scheduling delay, not an exact appointment. Busy main threads and background-tab policies can postpone callbacks. The setTimeout reference explains these constraints, which matter when testing elapsed time.

Implement a leading throttle with a trailing update

A leading throttle runs immediately when idle, then limits subsequent starts to one per interval. Adding a trailing call preserves the most recent value received during the waiting window. Without that final update, a slider or progress indicator can remain stuck slightly behind its final position.

function throttle(fn, wait = 100) {
if (!Number.isFinite(wait) || wait < 0) {
  throw new RangeError("wait must be a nonnegative finite number");
}
let lastRun;
let timer;
let latestArgs;
let latestThis;

function invoke() {
  timer = undefined;
  lastRun = performance.now();
  const args = latestArgs;
  const receiver = latestThis;
  latestArgs = latestThis = undefined;
  fn.apply(receiver, args);
}

function throttled(...args) {
  latestArgs = args;
  latestThis = this;
  const now = performance.now();
  const remaining = lastRun === undefined ? 0 : wait - (now - lastRun);

  if (remaining <= 0) {
    clearTimeout(timer);
    invoke();
  } else if (timer === undefined) {
    timer = setTimeout(invoke, remaining);
  }
}

throttled.cancel = () => {
  clearTimeout(timer);
  timer = latestArgs = latestThis = lastRun = undefined;
};
return throttled;
}

Use performance.now for elapsed-time measurement because it is monotonic rather than tied to wall-clock corrections. The Performance API reference describes the distinction. Here cancellation also resets the interval, so the next call runs immediately. That is a policy worth documenting rather than leaving implicit.

This throttle limits callback starts, not concurrent asynchronous work. If one request takes longer than the interval, several requests can overlap. Add concurrency control separately when the operation requires only one in-flight task.

Prevent stale results and release resources

Debouncing requests does not guarantee response order. An earlier search can finish after a later search and overwrite its results. Combine the timing policy with an AbortController and a monotonically increasing request identifier, checking the identifier before updating the interface.

Invalidate earlier work when new input arrives, not only when the delayed request finally starts. Otherwise, an old response can appear during the debounce window even though the input already changed. Cancellation saves resources when supported; the identifier check protects correctness when cancellation races with completion.

Keep event handlers and timer wrappers stable for as long as the listener is registered. Creating a fresh debounced function on every event creates independent timers instead of resetting one timer. Removing an event listener also requires the original function reference, as explained in the EventTarget documentation.

On teardown, remove the listener, cancel the pending wrapper, and abort any in-flight request. For a search input, copy event.currentTarget.value synchronously and pass the string into the wrapper. Delayed work should not depend on an event object's changing dispatch context.

Error handling belongs at the asynchronous operation boundary. A try/catch surrounding a call to the debounced wrapper cannot catch an exception thrown later by its timer callback. Handle expected request failures inside the invoked function and route unexpected errors through your application's reporting policy.

Test timing semantics instead of guessing delays

Write tests with fake timers so you can inspect call counts and arguments without waiting in real time. Ensure the test framework advances the monotonic clock as well as timers when exercising the throttle. A frozen performance.now can make a correct scheduling test behave unexpectedly.

For debounce, verify that several calls produce one callback with the latest arguments, no callback occurs before the interval, and cancel prevents pending work. Test a second burst after the first callback to catch state that was not reset correctly.

For throttle, verify immediate execution, one trailing update, no duplicate trailing call after a single invocation, and correct behavior after cancellation. Include a call just before the interval boundary and one immediately afterward. Test receiver preservation if your application uses methods rather than arrow functions.

Start with a modest delay as an experiment, then assess perceived responsiveness with actual users and devices. The development hub connects these helpers with wider application practices. Timing policy should be observable and documented: say whether the helper runs on the leading edge, trailing edge, both, or after a maximum wait. The word throttle alone does not specify enough behavior for a reliable shared utility.

Key takeaways

  • Debounce waits for silence; throttle permits bounded periodic updates.
  • Document leading, trailing, cancellation, and maximum-wait behavior.
  • Keep input feedback immediate while deferring expensive side effects.
  • Handle request ordering and cleanup independently of timer scheduling.
  • Test exact policies with controlled timers and realistic lifecycle transitions.
Featured sponsor

Reach designers & developers at work

Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.

Become a sponsor

Keep reading

Related articles

More in JavaScript & Frameworks
JavaScript & Frameworks

SSR vs SSG vs ISR vs SPA: Choosing a Rendering Strategy

Choose rendering strategies by freshness, personalization, and failure tolerance. Compare SSR, SSG, ISR, and SPA behavior with route-level examples and practical tests.

Includes: API latencySofia MarchettiSofia Marchetti
JavaScript & Frameworks

Loading Skeletons and Error States for Async API Data

Build resilient API interfaces with explicit loading states, stable skeletons, cancellation, and useful recovery actions that keep users in control.

Includes: API latencySofia MarchettiSofia Marchetti
JavaScript & Frameworks

AbortController: Cancel Fetch Requests and Set Timeouts

Cancel browser fetch requests, implement reliable timeouts, and prevent stale responses with AbortController while keeping cleanup and user-facing errors predictable.

Includes: API latencyDaniel KimDaniel Kim
JavaScript & Frameworks

Promise.all vs allSettled vs race vs any in JavaScript

Choose Promise.all, allSettled, race, or any based on failure behavior, then control concurrency and test asynchronous workflows without accidental serial execution.

Includes: API latencyDaniel KimDaniel Kim
DevOps & Deployment

10 Best Uptime Monitoring Tools

Compare ten uptime monitoring services for websites and APIs, from simple availability checks to incident workflows. Learn what each is suited to, what it cannot tell you, and how to choose.

Includes: API latencyRavi DesaiRavi Desai
Backend & APIs

12 Best Free Public APIs for Side Projects

Find approachable public APIs for weather, books, countries, and sample data, plus practical notes on access, limits, and choosing an endpoint for a side project.

Includes: API latencyChidi NwosuChidi Nwosu