Skip to content

Blog / Async Race Condition

Async race condition in search results

Sumeet Kumar Paul2 min read
The symptom
Results for an older query replace the results for the newest one.
The cause
Responses are committed in the order they arrive, not the order they were requested.
The fix
Give every request an identity and let only the latest one update state.

Problem

Under rapid input changes and slow network conditions, multiple search requests can remain in flight at the same time. When responses resolve out of order, older results may overwrite newer state, causing the UI to display incorrect data.

The implementation assumes that the most recent request will resolve last. This assumption breaks under real-world network latency and asynchronous execution.

  • Multiple async requests in flight simultaneously.
  • Variable or throttled network latency.
  • Rapid user input before previous requests resolve.
  • State updates tied directly to promise resolution.
page.tsx
srcapppracticeasync-race-condition-in-search-resultspage.tsx
1useEffect(() => {
2  if (!query.trim()) return;
3
4  void (async () => {
5    try {
6      const data = await fetchResults(query);
7      console.log("commit results for:", query);
8      setResults(data);
9    } catch (error) {
10      console.error("search request failed", error);
11    }
12  })();
13}, [query]);

This implementation works in happy paths but does not guard against out-of-order async resolution. Any response that resolves later is allowed to update state, regardless of when it was initiated.

Solution

Each request is assigned an identity, and state updates are only allowed from the most recent request. Responses that resolve out of order are ignored instead of committing stale data.

page.tsx
srcapppracticeasync-race-condition-in-search-resultspage.tsx
1const requestIdRef = useRef(0);
2
3useEffect(() => {
4  if (!query.trim()) {
5    requestIdRef.current = 0;
6    return;
7  }
8
9  const requestId = ++requestIdRef.current;
10
11  void (async () => {
12    try {
13      const data = await fetchResults(query);
14      if (requestId === requestIdRef.current) {
15        console.log("commit results for:", query);
16        setResults(data);
17      } else {
18        console.log("ignored stale response for:", query);
19      }
20    } catch (error) {
21      console.error("search request failed", error);
22    }
23  })();
24}, [query]);

By guarding state updates with request identity, the UI becomes resilient to async timing differences and network variability.

Check your own code

  1. 01Does any effect set state straight after an await?
  2. 02What happens if the second request finishes before the first?
  3. 03Have you tested fast typing with the network throttled?
  • async race condition
  • react state
  • frontend edge case
  • ui correctness
  • search
View the source on GitHub ↗

Written by Sumeet Kumar Paul

Senior Frontend Engineer from India. Frontend Lab is where I reproduce real interface bugs and work through the fix.

Share article:

Need a hand?

I build frontends like this for a living.

Get in touch

More from the lab