What this usually means
Race conditions in JavaScript emerge when multiple asynchronous operations (Promises, timers, callbacks, fetch requests) interact with shared state or depend on run order, and their completion order is nondeterministic. This usually happens because developers assume a certain sequence (e.g., network response A will finish before B because it was fired first), but the event loop and browser/network quirks violate this assumption in production. The bug often slips through local tests but explodes in real-world usage or CI where timing varies.
The first ten minutes — establish facts before touching code.
- 1Add sequence and timestamp logging to all async entry/exit points (e.g., log Date.now() at send/resolve of fetch calls).
- 2Re-run your failing test or manual repro with Chrome DevTools throttling (Network > Slow 3G, CPU throttling 4x).
- 3Instrument shared state (e.g., Redux store) with console.trace() on mutation points.
- 4Search the codebase for overlapping async triggers (e.g., onClick + onChange handlers firing in succession).
- 5Temporarily add artificial delays (await new Promise(r => setTimeout(r, N))) to simulate slow or reordered responses.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchsrc/api/ or services/ directory for fetch or XHR logic
- searchComponent files with useEffect/useState or setState calls
- searchCentral state store files (e.g., store.js, reducers/)
- searchJest or Cypress test cases with async/await or done() usage
- searchNetwork tab HAR exports (export HAR, look for overlapping requests)
- searchApplication logs in Datadog, New Relic, or whatever APM service you use
Practical causes, not theory. These are the things you will actually find.
- warningNot aborting stale fetches (missing AbortController or similar pattern)
- warningPromise chains resolving out of intended order due to missing await
- warningOverlapping React useEffect hooks running on improper dependency arrays
- warningEvent handlers mutating shared state without serialization/protection
- warningInsufficient locking or flag logic around critical async sections
- warningAssuming setTimeout or setImmediate will strictly order events
Concrete fix directions. Pick the one that matches your root cause.
- buildUse an AbortController to cancel out-of-date requests in flight when issuing new ones
- buildTrack a version/id for each async call; only update state if response version matches the latest issued
- buildAdd dependency arrays to useEffect so the effect only runs when truly needed
- buildGuard state updates by checking that the triggering input/event matches current state
- buildChain Promises with explicit awaits instead of relying on implicit sequencing
- buildRefactor event handlers so only one state update fires per user interaction
A fix you cannot prove is a guess. Close the loop.
- verifiedTurn on network and CPU throttling and verify correct UI ordering through 20+ rapid user action cycles
- verifiedRun the failing automated test 100+ times in a loop (e.g., jest --runInBand --repeat=100) to ensure it passes consistently
- verifiedCheck logs for absence of out-of-order or duplicated state transitions
- verifiedVerify that stale fetches are aborted (watch Network tab—should show canceled requests)
- verifiedHave a teammate code review for accidental shared state mutation or handler overlap
Things that make this bug worse or harder to find.
- warningPapering over with artificial timeouts instead of resolving the sequencing issue
- warningAssuming it’s fixed because it’s hard to reproduce locally
- warningLeaving verbose debug logs in production
- warningIgnoring test flakiness as 'ci/env issues' instead of an underlying race
- warningAdding more flags/booleans until the state machine is incomprehensible
Async Search Input Overwrites Results Out of Order
Timeline
- 09:00Customer reports search box sometimes shows wrong results after rapid typing.
- 09:15Can't reproduce locally at first; add timestamped logs to search action.
- 09:40Under network throttling, see results from earlier keystrokes replacing newer input.
- 10:10Realize that fast user input fires multiple fetches; responses return out of order.
- 10:30Patch adds fetch ID to each request; state updated only if response ID matches most recent.
- 11:00Tests pass; run 200 iterations in CI—no failures.
- 11:15Deploy hotfix; customer confirms bug is resolved.
I got a bug report about our search input showing the wrong results when users typed quickly. On my dev machine, everything worked fine, so at first I dismissed it as a minor cache issue.
When I added logs with fetch start/end times and simulated slow network in Chrome, I immediately saw the problem: rapid inputs triggered overlapping Axios requests, and older responses sometimes came back last, overwriting the newest results in Redux.
The fix was to tag each fetch with a unique incrementing ID, store the active one in Redux, and only commit results if the response’s ID matched—classic versioning. This killed the flakiness for good, and all tests passed even under extreme throttling.
Root cause
AJAX responses from stale keystrokes overwrote state because their asynchronous arrival order was unpredictable.
The fix
Implemented request versioning: Only applied response payload if its ID matched Redux's latest search version.
The lesson
Never trust async operation order—always guard shared state updates, especially after user-triggered concurrent requests.
Because JavaScript is single-threaded, many engineers dismiss race conditions as 'not our problem.' But once async I/O, timers, React hooks, or Promise chains come into play, all bets are off. The event loop queues can produce arbitrary execution orders, and network variability makes production behavior fundamentally nondeterministic.
The most insidious part: many local tests and manual QA passes won’t catch these problems. They only surface under load, slow network, or CI, where timing windows widen.
You won’t catch a race by staring at static code. Temporary, timestamped logs at every async entry/exit point are critical. Print out the request payload, when it’s sent, when it’s received, and the version or state at every mutation site.
Don’t be afraid to use console.trace() at the moment of state changes—this surfaces unexpected caller stacks and helps discover which handler or effect is firing out of turn.
Always tie async responses to some kind of request ID or sequence number. React, Redux, and Vue all support this pattern—store the current ID/version in local or global state and cross-verify before mutation.
For network requests, AbortController is the right primitive: cancel any in-flight requests when new input supersedes them. In React, useEffect cleanup functions should always abort pending operations.
In test code, wrap all asynchrony in explicit await or done()—never rely on 'the old promise will resolve first.'
In React, an easy-to-miss race is running a useEffect without an adequate dependency array, causing multiple overlapping effects during rapid prop/state changes.
In Redux, watch for thunks or sagas dispatching the same actions in parallel, and make sure reducers check action payloads for recency or version. MobX and RxJS introduce their own races if observables or subscriptions overlap without reference counting or teardown logic.
Frequently asked questions
Can JavaScript really have race conditions if it’s single-threaded?
Yes. The event loop's async callbacks, timers, and I/O mean code can interleave in arbitrary order, especially when dealing with shared state or overlapping network requests.
What’s the simplest way to prevent stale AJAX responses from overwriting state?
Attach an incrementing version or ID to each request; only update state if the incoming response matches the latest issued version. Or, cancel in-flight requests with AbortController.
Why do race conditions often appear in CI but not locally?
CI environments typically have different performance profiles, more parallelism, and network contention, increasing the likelihood of unexpected async orderings.
Is adding setTimeout to 'wait' for async operations a good solution?
No. setTimeout introduces more nondeterminism and can mask the real sequencing issue. Use explicit Promise chaining, version checking, or cancellation logic.