My users kept complaining that typing fast in the search bar showed the wrong results. Old API responses were arriving after new ones and overwriting the correct data.

  • What a race condition is in frontend development
  • Why rapid keystrokes trigger overlapping network requests
  • How to use AbortController to cancel stale REST API calls
  • How to keep your React UI in sync with the latest user input

What broke and what the user saw

I built a live search feature in React bundled with Vite, calling a remote REST API as the user typed. When a user typed 'apple' quickly, the app sent five separate network requests. Because network speeds vary, the request for 'app' finished last and replaced the correct results for 'apple'.

To the user, this looked like a glitch where the search results abruptly reverted to an older query. It made the app feel broken and untrustworthy.

Diagnosing the issue step by step

I added console logs to the search component's useEffect hook to track incoming network responses. I noticed that response logs were appearing out of order compared to the typing order. The app was blindly trusting every incoming response without checking if it was still relevant.

This is a classic race condition, which happens when two asynchronous operations compete to finish first, and the winner depends on unpredictable network timing. I needed a way to throw away old responses when a new search started.

useEffect(() => {
  const controller = new AbortController();
  
  async function fetchResults() {
    try {
      const response = await fetch(`/api/search?q=${query}`, {
        signal: controller.signal
      });
      const data = await response.json();
      setResults(data);
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('Fetch failed', error);
      }
    }
  }

  fetchResults();

  return () => {
    controller.abort();
  };
}, [query]);

This code uses an AbortController, a built-in browser tool that lets you cancel ongoing network requests before they finish. When the query changes, the cleanup function cancels the previous request so it never updates the state.

Common mistakes to watch out for

A common mistake is trying to solve this by tracking request order with an incrementing counter integer. While that prevents old data from rendering, it still leaves the old network requests running in the background, wasting user bandwidth. Always prefer AbortController for HTTP requests because it actually stops the useless data transfer.

Another trap is forgetting to catch and ignore the AbortError in your catch block. If you do not check for it, you will flood your console with annoying error messages every time a user types a new letter.

Open your current React project, find a search input or filter dropdown, and add an AbortController to see how much cleaner your network tab becomes.