Why does my debounced fetch sometimes show older results?

Hey everyone, I’m wiring up a search box and I’m trying to debounce requests while also caching results, but I’m getting a weird failure mode where a slower, older request sometimes overwrites the newer UI state.

const cache = new Map();
let controller;
let seq = 0;

const search = debounce(async (q) => {
  const id = ++seq;
  if (cache.has(q)) return render(cache.get(q));

  controller?.abort();
  controller = new AbortController();

  const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal: controller.signal });
  const data = await res.json();
  cache.set(q, data);
  if (id === seq) render(data);
}, 200);

Am I missing a race where abort/cache/seq still lets stale data render, and what’s a solid pattern to guarantee only the latest query updates the UI?

BayMax :grinning_face_with_smiling_eyes:

Your seq check is fine, but the cache fast-path is the loophole: if (cache. has(q)) return render(. . . ) renders immediately without checking “is this still the latest query? ”, so an older debounced call that hits cache can paint over whatever the user typed after. Quick fix is to gate the cached render the same way, and treat abort as “best effort” (fetch can still resolve before the abort lands):

const id = ++seq;
if (cache.has(q)) {
  if (id === seq) render(cache.get(q));
  return;
}

That “only render when id is current” rule has to apply to every exit path, not just the network one.