In this lesson, you will learn how to identify and prevent "race conditions," where the outcome of your code depends on the unpredictable timing of asynchronous tasks.
The Problem: When Order is Non-Deterministic
In Node.js, we love non-blocking I/O, but it comes with a trade-off: we cannot guarantee exactly when an asynchronous operation will finish. A race condition occurs when two or more asynchronous operations attempt to read or write the same shared resource (like a variable, file, or database record), and the final state depends on which operation completes first.
Imagine you are trying to increment a balance stored in a variable:
let balance = 100;
function updateBalance() {
// Simulate reading from a file or DB
setTimeout(() => {
balance = balance + 10;
console.log(`New balance: ${balance}`);
}, Math.random() * 100);
}
updateBalance();
updateBalance();
In this example, if the second call starts before the first one finishes reading the original balance, they both might see 100, calculate 110, and set the balance to 110 instead of 120. Because Math.random() dictates the timing, the result is non-deterministic—it changes every time you run the code.
Mitigating the Race
To prevent these "races," you must ensure that shared state is handled sequentially or protected. A simple strategy is to queue your operations or use a flag to signal that a resource is currently "in use."
If you need to update data, don't let multiple asynchronous flows touch that data at the same time. Instead, keep a "pending" status:
let isUpdating = false;
let balance = 100;
function updateBalance() {
if (isUpdating) return; // Simple lock
isUpdating = true;
setTimeout(() => {
balance += 10;
isUpdating = false;
}, 50);
}
By adding logic to control when an operation is allowed to proceed, you turn an unpredictable race into a controlled sequence. Always remember: in an event-driven loop, the finish line is rarely where you expect it to be.
Key Takeaway: A race condition occurs when shared data is modified by multiple asynchronous tasks whose execution order is not guaranteed; you must serialize these operations to ensure consistent state.