BugMojoBugMojoBugMojo
FeaturesPricingBlogHelpAbout
Add to ChromeLog inGet started
BugMojoBugMojo

Bug reports that actually help fix bugs — capture, replay, share.

A product of Softech Infra.

Product

  • Features
  • Pricing
  • Browser extension
  • Get started
  • Log in

Resources

  • Help & guides
  • Blog
  • Compare
  • Glossary

Company

  • About
  • Contact
  • Security
  • Privacy
  • Terms
  • Sitemap
© 2026 BugMojo. All rights reserved.
AllGuidesEngineeringPlaybooksCompareGlossaryAlternativesBy roleBug tracking by framework
  1. Home
  2. Blog
  3. Glossary
  4. What Is a Race Condition? (With Examples)
Glossary

What Is a Race Condition? (With Examples)

A race condition is a bug where the outcome depends on the unpredictable timing or interleaving of two or more operations that access the same shared state. Here are concrete examples, why they cause flaky tests, and how to prevent them on the frontend and backend.

Hrishikesh BaidyaHrishikesh Baidya·Jul 16, 2026·8 min read
Glossary
Isometric line-art timeline of two operations interleaving reads and writes on shared state, the final write cracked as a race corrupts the value, lime on a dark canvas
TL;DR
  • A race condition is a bug where the outcome depends on the unpredictable timing or interleaving of two or more operations that touch the same shared state.
  • Common shapes: a stale async response overwriting a newer one, check-then-act (TOCTOU), a double-submit before a button disables, and a read-modify-write without a transaction.
  • They are hard because they are intermittent, timing-dependent, and 'work on my machine' — and a leading cause of flaky tests.
  • Prevent them by making ordering explicit: transactions, atomic ops, locks, debounce/disable, AbortController, idempotency keys, optimistic locking, and await.

Definition

A race condition is a bug where a program's outcome depends on the unpredictable timing or interleaving of two or more operations that access the same shared state. When they run in the assumed order it works; when they interleave differently you get a stale value, a lost update, or a crash.

The standards-body phrasing is precise. MDN defines a race condition as behavior where 'the output is dependent on the sequence or timing of other uncontrollable events.' The two load-bearing words are uncontrollable and timing: your code assumes an order of operations, but nothing actually enforces that order, so the real order is decided by whatever happens to be faster at runtime — the network, the scheduler, the disk, the user's click speed. When the fast path lines up with your assumption, the bug hides. When it does not, the program produces a wrong result even though every individual line is correct.

The shared state is the other half of the definition. A race needs at least two operations and something they both read or write — a variable, a piece of React state, a row in a table, a file, a cache entry. The general concept calls the overlapping window a critical section: the stretch of code where concurrent access to that shared resource must be serialized, or the result becomes undefined. If two operations never touch the same state, they can interleave all they like and nothing breaks. The bug only exists where timing and sharing meet.

Four concrete examples

1. Stale async response (last-write-wins). Two requests update the same state, and the older one happens to return last, so its stale result clobbers the newer one. This is the single most common race in frontend code — a search box, a tab switch, a filter — because responses do not return in the order they were sent.

SearchResults.tsxtsx
// BUG: whichever request RESOLVES last wins, not whichever was SENT last.
function onQueryChange(query: string) {
  fetch(`/api/search?q=${query}`)
    .then((r) => r.json())
    .then((results) => {
      // Type 'react' (slow) then 'redux' (fast): 'redux' returns first,
      // then the slow 'react' response lands and overwrites it.
      setResults(results); // stale response wins
    });
}

2. Check-then-act (TOCTOU). You read a value, then act on it — but the value can change between the check and the act. Time-of-check to time-of-use is the formal name for this race, and it is the root of countless 'but I just checked that it existed' bugs.

reserve.tsts
// BUG: another request can grab the last seat between the check and the act.
async function reserveSeat(eventId: string) {
  const seats = await getAvailableSeats(eventId); // time of CHECK
  if (seats > 0) {
    // ...another request runs here and books the last seat...
    await bookSeat(eventId); // time of USE — now oversold
  }
}

3. Double-submit before the button disables. The user double-clicks 'Pay' faster than the handler flips disabled, so two identical requests go out. Two charges, one order.

CheckoutButton.tsxtsx
// BUG: the second click fires before React re-renders with disabled=true.
function CheckoutButton() {
  const [submitting, setSubmitting] = useState(false);
  async function onClick() {
    if (submitting) return; // guards later clicks, but the FIRST two race
    setSubmitting(true);    // state update is async — not applied instantly
    await pay();            // a fast double-click can enter twice
  }
  return <button onClick={onClick}>Pay</button>;
}

4. Read-modify-write without a transaction. Two backend requests read a counter, both increment it in application code, and both write back. One increment is silently lost — the classic lost-update race on a database row.

increment.tsts
// BUG: two concurrent requests both read 41, both write 42. Lost update.
async function incrementViews(postId: string) {
  const post = await db.post.findUnique({ where: { id: postId } });
  const next = post.views + 1;          // both read the same value
  await db.post.update({                // both write it back
    where: { id: postId }, data: { views: next },
  });
}

Why race conditions are so hard

Race conditions are the debugging nightmare that a stack trace cannot solve, and the reason is structural: the bug is in the timing, not in any single line. Reproduce it and the timing shifts and it vanishes. That is what 'works on my machine' actually means for a race — your machine happens to schedule the operations in the order your code assumed, so the fast path always wins locally, while a loaded server or a slower network flips the order and the bug appears. The failure is intermittent by definition, so it slips past code review, past a green test suite, and into production, where it shows up as a mysterious one-in-a-thousand wrong number.

Races are a leading cause of flaky tests

A flaky test passes and fails without any code change, and an unhandled race is one of the top reasons. A test that awaits async work, a timer, or an animation depends on operations finishing in a particular order; when nothing enforces that order, the test passes when the timing lines up and fails when it does not — most often only on CI, which schedules differently than your laptop. If you are hunting intermittent red builds, start with our guide to debugging flaky tests. The flake is usually not noise; it is a real race surfacing under different timing.

How to prevent them

Every fix comes down to the same idea: stop leaving the order to chance and make it explicit, so concurrent access to shared state is serialized or made safe. The specific tool depends on where the race lives.

On the backend, wrap read-modify-write sequences in a database transaction so the read and write are atomic, or push the arithmetic into a single atomic operation the database serializes for you. Use optimistic locking — a version column that makes a stale write fail loudly — or a lock/mutex for a true critical section. Add an idempotency key so a retried or duplicated request cannot apply its effect twice.

increment.fixed.tsts
// FIX: one atomic UPDATE. The database serializes concurrent increments —
// no read-modify-write window, so no lost update.
await db.post.update({
  where: { id: postId },
  data: { views: { increment: 1 } },
});

// Or optimistic locking: only write if the row is still the version we read.
// UPDATE post SET views = 42, version = version + 1
//   WHERE id = ? AND version = 41;   -- 0 rows updated => retry

On the frontend, disable the submit button on the first click (and dedupe server-side too, since a determined user can beat any UI guard). Debounce rapid input so you fire one request instead of ten. Sequence dependent steps with await so step two never starts before step one finishes. And for the stale-response race, cancel in-flight requests with AbortController so an older response can never overwrite a newer one.

SearchResults.fixed.tsxtsx
// FIX: abort the previous request before starting a new one. A stale
// 'react' response is aborted the moment 'redux' fires — it can't win.
useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/search?q=${query}`, { signal: controller.signal })
    .then((r) => r.json())
    .then(setResults)
    .catch((e) => { if (e.name !== 'AbortError') throw e; });
  return () => controller.abort(); // cancel on the next keystroke
}, [query]);

How this shows up in a real BugMojo bug report

Here is the honest limit of a stack trace on a race condition. A trace tells you where code ran and the call path that got there — but a race's root cause is the interleaving of two operations, and a single trace only ever captures one of them at one moment. The state was corrupted by the order in which requests resolved, and that order does not appear in a stack frame. So the frame looks innocent and the bug looks impossible.

In a BugMojo report the trace does not arrive alone. The browser extension captures the failure with its surrounding state — an rrweb session replay, the console output, and the full network timeline — so you can literally see the two requests overlap: GET /api/search?q=react sent first, GET /api/search?q=redux sent second, and the react response resolving last and overwriting the newer one. That interleaving is the whole bug, and it is exactly what a stack trace alone can never reveal. Pair that timeline with a session replay and 'it randomly shows the wrong results' becomes 'the older request wins the race — cancel it with AbortController.'

FeatureCapabilityBugMojoStack trace alone
Shows where code failed—✓✓
Network timeline showing request interleaving—✓—
rrweb session replay of the exact interaction—✓—
Console + network captured with the failure—✓—
Captured bug bundle handed to an AI agent over MCP—✓—
For a race condition, the interleaving is the evidence — and it lives in the network timeline and session replay, not the stack trace.
Key takeaway

A race condition is degradation caused by timing, so the evidence is the interleaving, not the line that threw. Make ordering explicit: transactions and atomic ops for read-modify-write, optimistic locking and idempotency keys for duplicate effects, and debounce, disable-on-submit, and AbortController on the client. When you reach for an AI agent, give it the network timeline and replay — the two requests overlapping — not just the trace, because the frame that failed is rarely the operation that raced.

See the race, not just the stack trace

BugMojo captures the failing session with its rrweb replay, console, and full network timeline — so you can watch the two requests interleave — then hands the whole bundle to Claude Code or Cursor over MCP, so your agent reads the actual ordering behind the race and can fix it.

Install the extension

Frequently asked questions

Frequently asked questions

Sources

  1. Race condition — "the behavior of a system where the output depends on the sequence or timing of other uncontrollable events" (MDN Glossary) — MDN Web Docs (2026)
  2. Race condition — critical sections, data races, and shared-resource timing (overview) — Wikipedia (2026)
  3. AbortController — signal in-flight fetch requests to cancel so a stale response cannot win — MDN Web Docs (2026)
  4. Time-of-check to time-of-use (TOCTOU) — the check-then-act race where state changes between the two steps — Wikipedia (2026)
Share:
Hrishikesh Baidya
Hrishikesh Baidya· Chief Technology Officer

Hrishikesh Baidya is the CTO at Softech Infra. He is drawn to architecture that is invisible — systems that simply work — and leads the engineering behind BugMojo.

On this page

  • Definition
  • Four concrete examples
  • Why race conditions are so hard
  • How to prevent them
  • How this shows up in a real BugMojo bug report

Get bug-tracking insights, weekly.

Engineering deep-dives, QA playbooks, and honest tool comparisons. No spam — unsubscribe in one click.