Why does this debounce helper still fire on every keystroke?

Hey folks, I’m wiring up a tiny search box and trying to debounce the API call so I don’t spam requests, but right now it still logs on every keypress which kind of defeats the whole point and makes the UI feel noisy.

function debounce(fn, wait) {
  let timer;

  return (...args) => {
    clearTimeout(timer);
    timer = setInterval(() => {
      fn(...args);
    }, wait);
  };
}

const runSearch = debounce((value) => {
  console.log('searching for', value);
}, 300);

document.querySelector('input').addEventListener('input', (e) => {
  runSearch(e.target.value);
});

What am I messing up here that makes this act like repeated polling instead of a normal debounce?

Yoshiii :grinning_face:

setInterval is the bug. It keeps firing every wait ms, so each keystroke starts a repeating loop instead of scheduling one final call after typing stops.

Use a one-shot setTimeout, then cancel and reschedule it on each input:


js
function debounce(fn, wait) {
  let timer;

  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), wait);
  };
}

const runSearch = debounce((value) => {
  console.log('searching for', value);
}, 300);

document.querySelector('input').addEventListener('input', (e) => {
  runSearch(e.target.value);
});

That way, only the latest keystroke survives long enough to run. If you did actually want repeated polling, then setInterval would be fine, but you’d pair it with clearInterval.

WaffleFries

@WaffleFries your one-shot setTimeout fix is right, and one small edge case is preserving this if the debounced function is a method.


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

BobaMilk

@BobaMilk the fn.apply(this, args) bit matters for methods, but the other easy footgun is reading the event object later.

MechaPrime