Tags: web-dev concept

Race Conditions

Date: 2026-08-17


A bug whose outcome depends on the order two operations happen to finish in. They pass every test, work on fast connections, and appear in production on the slow ones — which is why they’re diagnosed from user reports rather than from stack traces.


A race condition occurs when the correctness of a result depends on timing that isn’t controlled. Nothing errors; the wrong thing simply wins.

user types "sh"      → request A sent
user types "shoe"    → request B sent

FAST PATH               SLOW PATH
A returns  (shirts)     B returns (shoes)
B returns  (shoes)      A returns (shirts)
    ↓                       ↓
shows shoes ✓           shows SHIRTS ✗
                        for the query "shoe"

Nothing failed. Both requests succeeded; the older one just landed last and overwrote the newer result. On a fast connection this almost never happens, which is why it survives development.

The fixes, in order of preference

1  CANCEL the stale request
     AbortController — the request stops,
     its handler never runs

2  IGNORE stale responses
     tag each request; discard any
     response that isn't the latest

3  DEBOUNCE the trigger
     fewer requests, smaller window
     ← reduces the odds, doesn't remove
       the race

Debouncing alone is not a fix. It makes the bug rarer, which makes it harder to find. Cancel or discard.

The other shapes

Read-modify-write. Two operations read the same value, both modify their copy, and the second write erases the first:

stock = 10

A reads 10            B reads 10
A computes 10 − 1     B computes 10 − 3
A writes 9            B writes 7
                      ↑ A's sale is gone

The fix is not application code — it’s the database doing the arithmetic (SET stock = stock - 1), or a transaction with appropriate isolation, or optimistic locking with a version column — Transactions and ACID.

Check-then-act. The gap between checking a condition and acting on it:

if (!(await userExists(email)))   // check
   await createUser(email)        // act

Two requests can both pass the check before either creates the user.

The fix is a unique constraint in the database, and handling the conflict. A check in application code cannot close this gap.

Component teardown. An async result arriving after the thing that asked for it has gone — a state update on an unmounted component, or a handler writing into a DOM node that no longer exists.

Double submission. A user clicks twice; two orders. Disable on submit, and enforce with an idempotency key server-side, because the disable is a UI courtesy rather than a guarantee — Idempotency.

Why they’re hard

  • They pass tests, because tests are fast and deterministic
  • They don’t reproduce locally, where latency is near zero
  • They present as data problems — wrong stock, duplicate order, stale UI — so they get investigated as data bugs
  • The user can’t describe them. “It showed the wrong thing” arrives without the sequence that caused it

The diagnostic habit: when a bug is intermittent and involves two things that can happen in either order, assume a race before assuming anything else. Throttling the network in DevTools to a slow profile makes most front-end races reproducible immediately.

Not a threading problem here

Genuine data races — two threads writing the same memory simultaneously — can’t happen in JavaScript, because there’s one thread — Concurrency and Parallelism.

What remains is ordering, and it’s just as capable of showing a customer the wrong price. Single-threaded runtimes remove a class of race and leave this one entirely intact, which is worth being clear about — the safety guarantee is narrower than it’s often described.

Prevention worth building in

  • AbortController on every request tied to changing input — search, filters, autocomplete
  • Let the database enforce uniqueness and arithmetic, rather than the application
  • Idempotency keys on anything that creates or charges
  • Cancel on teardown in every component that fetches
  • Test on a throttled connection before shipping anything with concurrent requests