Race Condition
In short
A race condition is a bug where a program's result depends on the unpredictable timing of threads, processes, or requests that use shared data at the same time.
What is a race condition?
A race condition happens when two or more operations run concurrently, touch the same data or resource, and at least one of them changes it, so the result depends on which one happens to go first. Because the timing changes from run to run, the program may work correctly thousands of times and then fail once, which makes race conditions some of the hardest bugs to reproduce.
The classic case is counter += 1, which is really three steps: read the value, add one, and write it back. If two threads both read 5, both write 6, and one increment is lost. Another common pattern is check-then-act, such as checking that a file doesn't exist and then creating it, while another process creates it in between; this is called time-of-check to time-of-use (TOCTOU) and is also a well-known class of security vulnerability. Races are not limited to threads: two web requests buying the last concert ticket, or two processes writing the same file, can race too.
Imagine two people sharing a bank account who check the balance of $100 at two ATMs at the same moment. Both see enough money, both withdraw $80, and without coordination the bank lets both happen. Fixes include mutexes and other locks, atomic operations, database transactions with a suitable isolation level or optimistic locking, and designs that avoid shared mutable state, such as immutable data or message passing. Tools such as thread sanitizers and Go's race detector help find races during testing.
Race conditions are often mixed up with deadlocks. With a race condition the program keeps running but produces wrong results; with a deadlock it stops making progress. The two are linked, because adding locks to fix a race can create a deadlock if they are acquired in inconsistent orders. Mutexes and semaphores are the tools; the race condition is the bug they prevent.
At a glance
Key takeaways
- A race condition makes the outcome depend on the timing of concurrent operations.
- Read-modify-write and check-then-act sequences are the most common causes.
- Races can happen between threads, processes, or separate web requests.
- Locks, atomic operations, and transactions are the usual fixes.
- A race gives wrong results, while a deadlock stops progress entirely.
Example
// Two requests try to buy the last ticket at the same time
async function buyTicket(userId) {
const event = await db.getEvent(1); // both requests read seatsLeft = 1
if (event.seatsLeft > 0) { // both pass the check...
await db.createOrder(userId);
await db.updateEvent(1, { seatsLeft: event.seatsLeft - 1 });
}
}
await Promise.all([buyTicket("ada"), buyTicket("linus")]); // 2 orders, 1 seat
// Fix: make the check and the update one atomic step in the database:
// UPDATE events SET seats_left = seats_left - 1
// WHERE id = 1 AND seats_left > 0
// ...and create the order only if exactly one row was updated.Readers ask
What is the difference between a race condition and a deadlock?
A race condition lets the program keep running but can corrupt data or produce wrong results. A deadlock freezes the threads involved because each waits for a resource another one holds.
Can race conditions happen in single-threaded JavaScript?
Yes. Code never runs in parallel on one thread, but every await lets other tasks run in between, so two async operations can still interleave around shared data or an external database.
What is the difference between a data race and a race condition?
A data race is a specific low-level case: two threads access the same memory at the same time without synchronization, and at least one writes. A race condition is the broader problem of timing-dependent results, and it can exist even when every individual access is properly synchronized.
See also
- MutexOperating Systems, p. 20A mutex is a lock that lets only one thread at a time enter a critical section of code, so threads can't corrupt shared data by changing it at the same time.
- DeadlockOperating Systems, p. 8A deadlock is a situation where two or more threads or processes wait forever for each other to release resources, so none of them can make progress.
- ConcurrencyProgramming Fundamentals, p. 12Concurrency is a program's ability to make progress on several tasks in overlapping time periods, such as serving many users at once rather than one at a time.
- ThreadOperating Systems, p. 33A thread is the smallest unit of execution an operating system can schedule, running inside a process and sharing that process's memory with other threads.
- Optimistic LockingDatabases, p. 32Optimistic locking is a concurrency technique that lets transactions proceed without holding locks and checks a version number at save time to detect conflicts.
- Flaky TestTesting & Quality, p. 10A flaky test is an automated test that sometimes passes and sometimes fails without any change to the code, which makes its results hard to trust.
Spotted a mistake or something missing on this page?Suggest an edit