Managing Out-of-Order Responses & Race Conditions — Prioritizing the Right Request

This article delves into the conundrum of out-of-order network responses and the prevention of race conditions in contemporary applications. It outlines patterns for ensuring the most recent request takes precedence using refs, counters, and abort strategies.

ShareShare

Managing Out-of-Order Responses & Race Conditions — Prioritizing the Right Request

Contemporary User Interfaces (UIs) are characterized by a multitude of concurrent network calls, providing fertile ground for one of the most elusive bugs: race conditions.

A race condition manifests when various asynchronous operations compete with each other, and the operation completing first takes precedence, even if it logically shouldn't.

Consider the following scenario:

  • A user enters “dog” → a fetch for “dog” begins
  • The user then modifies the input to “do” → a fetch for “do” commences later, yet returns first
  • The UI displays the results for “do”, even though “dog” is the most recent input

This example illustrates a race condition induced by out-of-order responses.

In this comprehensive guide, we will dissect the following aspects:

  • The genesis of race conditions
  • Detection and reproduction techniques
  • Patterns to cancel, compare, or ignore stale responses
  • Implementations in React using refs, tokens, and counters
  • Real-world strategies employed in search inputs, autocompletes, and filters

The Significance of Race Conditions

Race conditions are insidious, gradually eroding user trust. A user may type a query and see an incorrect result, click to sort, only to have the old response supersede the new one, or open a modal, and stale data populates it.

Most users won't file a bug report; they'll just presume your application is defective.


The Breeding Grounds for Race Conditions

Race conditions commonly occur in the following scenarios:

  • Search inputs: Fast typing triggers numerous requests.
  • Dropdown filters: Rapid selection and reselection.
  • Tabs or modals: Quick navigation.
  • Rapid navigation: A client-side router with prefetch.
  • Parallel data fetching: Combined effects or compound queries.

Example of the Problem

Consider the following code snippet:

useEffect(() => {
  fetch(`/api/search?q=${query}`)
    .then(res => res.json())
    .then(setResults);
}, [query]);

This code executes a fetch for each query modification — but doesn't cancel the previous one. If older fetches resolve last, they supersede new ones, leading to an erroneous display of results.


Mitigation Strategies

1. AbortController (with fetch)

The AbortController class allows you to abort one or more Web requests as and when desired.

useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/search?q=${query}`, { signal: controller.signal })
    .then(res => res.json())
    .then(setResults)
    .catch(err => {
      if (err.name === "AbortError") return;
    });

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

This code snippet ensures the cancellation of stale requests before initiating a new one.


2. Request ID Tracking

By using a ref, you can keep track of the current request:

const requestIdRef = useRef(0);

useEffect(() => {
  const id = ++requestIdRef.current;

  fetch(`/api/search?q=${query}`)
    .then(res => res.json())
    .then(data => {
      if (id === requestIdRef.current) setResults(data);
    });
}, [query]);

This approach ensures that only the most recent request can modify the state.


3. Sequence Token Pattern

Creating a makeCancelable wrapper around each request can also prove effective:

function makeCancelable(promise) {
  let hasCanceled = false;
  const wrapped = new Promise((resolve, reject) => {
    promise.then((val) => (hasCanceled ? null : resolve(val)));
  });
  return {
    promise: wrapped,
    cancel: () => (hasCanceled = true)
  };
}

This method allows you to race and cancel older requests when necessary.


Race-Aware Libraries

React Query

React Query automatically cancels in-flight queries on refetch and deduplicates stale results.

useQuery(["search", query], fetchSearch, { keepPreviousData: true });

SWR

SWR ensures that the latest response prevails by using timestamps and cache keys. Its mutate() method respects freshness.


Real-World Implementations

GitHub Autocomplete

While typing in the issue search bar on GitHub, the input is debounced, prior fetches are cancelled, and a race-safe cache layer is used.

VSCode Extensions Marketplace

When filtering by publisher or keyword, old results are cancelled to avoid UI flashing from stale entries.

Notion Link Picker

As you type to find a page in Notion, only the latest result is displayed, eliminating any jumping or flickering due to out-of-order results.


Anti-Patterns

Here are some practices to avoid when dealing with race conditions:

  • Triggering fetch without a cancellation or ref guard
  • Neglecting stale responses
  • Updating state after a component unmount, leading to a potential memory leak
  • Using async/await in useEffect without cleanup
  • Responding to multiple input changes without debounce

Conclusion: Control the Timeline

Race conditions are not about speed — they're about priority.

When managing network requests, you are essentially coordinating a timeline:

  • When does a request start?
  • When does it end?
  • What else transpires in between?

The answer isn’t “first come, first served.”

The correct approach is “latest wins.”

Your UI should reflect this policy, ensuring an accurate and intuitive user experience.

Subscribe for Deep Dives

Get exclusive in-depth technical articles and insights directly to your inbox.

about

Ehsan Hosseini

Ehsan Hosseini

me [at] ehosseini [dot] info

Staff Software Engineer and Tech Lead with a track record of leading high-performing engineering teams and delivering scalable, end-to-end technology solutions. With experience across web, mobile, and backend systems, I help companies make smart architectural decisions, scale efficiently, and align technical strategy with business goals.

© 2025 Ehsan Hosseini. All rights reserved.