A simple debounce helper

function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

Every call clears the pending timer and starts a fresh one, so fn only runs once the calls have been quiet for delay milliseconds.

Using it on a search input

const onSearch = debounce((event) => {
  console.log("searching for", event.target.value);
}, 300);

input.addEventListener("input", onSearch);

Now the search only runs 300ms after the user stops typing, instead of on every keystroke.

Debounce vs throttle

// Debounce: runs once, after activity stops.
// Throttle: runs at most once per interval, during activity.

Choose debounce when you only care about the final state, and throttle when you want steady updates while something is happening.

Which approach should you use?

  • Debounce — search-as-you-type, validating a field, saving a draft.
  • Throttle — scroll position, mouse-move, or resize updates.
  • Common mistake: recreating the debounced function on every render, which resets its timer every time.

Frequently asked questions

What delay should I use?

Around 200 to 400 milliseconds feels responsive for typing. Use a longer delay for expensive work and a shorter one when snappiness matters.

Why does my debounce never fire?

Usually because a new debounced function is created on each call, so the timer keeps resetting. Create it once and reuse the same reference.

Want to understand closures and timers? Our free JavaScript course breaks them down clearly.