DEV Community

Cover image for Why Your JavaScript Debounce Function Still Sends Multiple API Requests (And How to Fix It)
Emily Scott
Emily Scott

Posted on

Why Your JavaScript Debounce Function Still Sends Multiple API Requests (And How to Fix It)

A strange problem many developers face is this:

You add debounce to your search input, but multiple API requests still get sent.

You expected one clean request.

Instead, the Network tab shows several requests.

This becomes frustrating in:

  • Search suggestions
  • Product filters
  • Live dashboards
  • Auto-save forms
  • Analytics tracking
  • Username availability checks
  • Real-time validation

Everything looks correct.

The debounce function exists.

But the bug still happens.

This problem is less discussed, and many developers misdiagnose it.

Let’s fix it step by step.


The Problem

Suppose you build a search feature.

You write this:

function debounce(fn, delay) {
  let timer;

  return function () {
    clearTimeout(timer);

    timer = setTimeout(() => {
      fn();
    }, delay);
  };
}

const search = debounce(() => {
  fetchResults();
}, 500);

input.addEventListener("input", search);
Enter fullscreen mode Exit fullscreen mode

You expect:

Only one API request after the user stops typing.

But instead:

  • Multiple requests still fire
  • Old results sometimes appear
  • The UI feels inconsistent

Very confusing.

It feels like debounce is broken.

But usually, the real problem is somewhere else.


Why This Happens

Debounce only delays function execution.

It does not cancel already running requests.

That is the key issue.

Example:

  1. User types "jav"
  2. A request starts
  3. User types "javascript"
  4. A new request starts later
  5. The older request finishes last

Now stale data appears.

Debounce reduced requests, but it did not solve race conditions.

This is the hidden bug.


Common Mistake #1: Debouncing Only the Function Call

Developers often think this is enough:

const search = debounce(fetchResults, 500);
Enter fullscreen mode Exit fullscreen mode

It is not.

Because once fetch() starts, debounce has no control over it.

The network request continues.

This is a very important distinction.


Step 1: Combine Debounce with Request Cancellation

You need both:

  • Debounce β†’ reduce unnecessary calls
  • AbortController β†’ cancel stale requests

Together, they solve the real issue.

Not separately.


Step 2: Use the Correct Real-World Solution

Use this:

let controller;

function debounce(fn, delay) {
  let timer;

  return function (...args) {
    clearTimeout(timer);

    timer = setTimeout(() => {
      fn(...args);
    }, delay);
  };
}

const search = debounce((query) => {
  if (controller) {
    controller.abort();
  }

  controller = new AbortController();

  fetch(`/api/search?q=${query}`, {
    signal: controller.signal
  })
    .then((res) => res.json())
    .then((data) => {
      renderResults(data);
    })
    .catch((error) => {
      if (error.name !== "AbortError") {
        console.error(error);
      }
    });
}, 500);
Enter fullscreen mode Exit fullscreen mode

Now stale requests are canceled.

Only the latest result matters.

This is the real production-safe solution.


Common Mistake #2: Recreating Debounce on Every Render

This happens constantly in React.

Example:

function Search() {
  const search = debounce(fetchResults, 500);
}
Enter fullscreen mode Exit fullscreen mode

Problem:

Every render creates a new debounce function.

That resets the timer.

Debounce stops working correctly.

Very common bug.


Step 3: Keep Debounce Stable in React

Better approach:

const debouncedSearch = useMemo(() =>
  debounce(fetchResults, 500),
[]);
Enter fullscreen mode Exit fullscreen mode

Now the debounce function stays stable.

Much better.

Much safer.

Less debugging.


Another Hidden Problem: Losing Input Value

This happens too:

input.addEventListener("input", debounce(() => {
  console.log(input.value);
}, 500));
Enter fullscreen mode Exit fullscreen mode

Sometimes the value is outdated.

Because the delayed function runs later.

Safer approach:

Pass the value directly.

input.addEventListener("input", (e) => {
  search(e.target.value);
});
Enter fullscreen mode Exit fullscreen mode

Always pass fresh values.

Very important.


Quick Debug Rule

Whenever debounce feels broken, ask yourself:

Am I delaying execution, or am I actually controlling stale requests?

That question usually reveals the real bug.

Most developers miss this.


Final Thoughts

Debounce is helpful, but incomplete by itself.

Remember:

  • Debounce delays function calls
  • It does not cancel running requests
  • Use AbortController for stale fetch cancellation
  • Keep debounce stable in React
  • Pass fresh values, not delayed references

This problem is tricky because the code looks correct.

But production behavior says otherwise.

Fixing this properly creates much better UX and much cleaner frontend logic.


Your Turn

Have you ever added debounce and still seen multiple API requests?

That is usually where the real lesson begins.

Peace,
Emily Idioms

Top comments (0)