jsguides

How to debounce input with setTimeout()

To debounce input events with setTimeout(), wrap your handler in a function that clears and resets a timer on every call. Each keystroke resets the timer, so expensive work only runs after the user stops typing. Use a debounce when an input fires too often and you only want to run work after the user pauses.

Recipe

function debounce(fn, delay = 250) {
  let timerId;
  return (...args) => {
    clearTimeout(timerId);
    timerId = setTimeout(() => fn(...args), delay);
  };
}

const runSearch = debounce((value) => {
  console.log("Search:", value);
}, 250);

input.addEventListener("input", (event) => {
  runSearch(event.target.value.trim());
});

Each new keystroke cancels the previous timer and starts a new one, so the search only runs after typing settles.

How debouncing works

Debouncing is useful when the latest value matters more than intermediate values. Every time the event fires, the previous timeout is cleared and a new one is scheduled. That means the wrapped function only runs after a quiet period has passed. In practice, this keeps expensive work such as search requests, layout updates, or analytics calls from running on every keystroke.

The important tradeoff is delay. A shorter delay feels more responsive, but it also allows more calls through if the user pauses frequently. A longer delay reduces work further, but it can make the interface feel sluggish. The best value usually depends on how costly the downstream action is and how quickly the user expects feedback.

Common edge cases

If the input element is cleared, the debounced function still receives an empty string, so your handler should decide whether to ignore it or show a default state. If the same debounced wrapper is reused for multiple inputs, they will share a single timer, which is usually not what you want. For separate fields, create a dedicated wrapper for each one so their timing stays independent.

Debounce is also different from throttle. The code below shows the difference — debounce waits for quiet, while throttle fires at most once per interval regardless of how many events arrive:

function throttle(fn, interval = 250) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= interval) {
      last = now;
      fn(...args);
    }
  };
}

// Debounced scroll — fires once after scrolling stops
window.addEventListener("scroll", debounce(() => {
  console.log("Debounced — user stopped scrolling");
}, 300));

// Throttled scroll — fires at most every 300ms while scrolling
window.addEventListener("scroll", throttle(() => {
  console.log("Throttled — periodic update");
}, 300));

That difference matters for scroll handlers, resize handlers, and search boxes, where you usually care about the final value rather than every transient update.

See also