Handling Race Conditions Using Node.js — Yes, You Read It Right.
Have you ever thought about what could happen if two people tried to withdraw from the same bank account at the exact same time? That’s a classic case of a race condition — and if not handled properly, it can lead to real money vanishing (or duplicating!) out of thin air. In this post, I’ll walk you through: What a race condition is (in the context of banking) How to simulate one in Node.js How to fix it using a mutex (lock). What Is a Race Condition? A race condition occurs when two or more operations access shared data at the same time, and the final result depends on the order in which they execute. In banking terms: Two people transfer money from the same account at the same time. If not handled properly, both could think the money is still there — and withdraw it — causing the account to go negative or become inconsistent. Simulating the Problem in Node.js Let’s say we have a shared in-memory balance of $100, and two transfer requests come in at the same time: const express = require('express'); const app = express(); const port = 3000; let accountBalance = 100; function transfer(amount) { const balance = accountBalance; if (balance < amount) { throw new Error('Insufficient funds'); } // Simulate some delay in processing. It can be many reason setTimeout(() => { accountBalance = balance - amount; console.log(`Balance after transfer: $${accountBalance}`); }, 1000); } app.post('/transfer', (req, res) => { const amount = 50; try { transfer(amount); res.send('Transfer completed'); } catch (error) { res.status(400).send(error.message); } }); app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); }); From your terminal - curl -X POST http://localhost:3000/transfer & curl -X POST http://localhost:3000/transfer & Did you notice? You still have 50$! This is broken! If two requests run simultaneously, both read $100, both think there's enough, and both deduct $50 — ending up with a final balance of $50, when it should be $0. Let's fix the problem - const { Mutex } = require('async-mutex'); const mutex = new Mutex(); async function transfer(amount) { const release = await mutex.acquire(); try { const currentBalance = accountBalance; if (currentBalance < amount) { throw new Error('Insufficient funds'); } await new Promise(resolve => setTimeout(resolve, 100)); accountBalance = currentBalance - amount; console.log(`Transfer successful. New balance: $${accountBalance}`); } finally { release(); } } Now, only one transfer runs at a time. The second request waits until the first finishes. And you may have noticed - your balance is now zero. This is a hypothetical example, but race conditions like this do happen in real-world applications — such as counting page views, managing product stock in e-commerce platforms, and more. The scary part? They often go unnoticed until they cause serious issues.

Have you ever thought about what could happen if two people tried to withdraw from the same bank account at the exact same time?
That’s a classic case of a race condition — and if not handled properly, it can lead to real money vanishing (or duplicating!) out of thin air.
In this post, I’ll walk you through:
- What a race condition is (in the context of banking)
- How to simulate one in Node.js
- How to fix it using a mutex (lock).
What Is a Race Condition?
A race condition occurs when two or more operations access shared data at the same time, and the final result depends on the order in which they execute.
In banking terms:
Two people transfer money from the same account at the same time. If not handled properly, both could think the money is still there — and withdraw it — causing the account to go negative or become inconsistent.
Simulating the Problem in Node.js
Let’s say we have a shared in-memory balance of $100, and two transfer requests come in at the same time:
const express = require('express');
const app = express();
const port = 3000;
let accountBalance = 100;
function transfer(amount) {
const balance = accountBalance;
if (balance < amount) {
throw new Error('Insufficient funds');
}
// Simulate some delay in processing. It can be many reason
setTimeout(() => {
accountBalance = balance - amount;
console.log(`Balance after transfer: $${accountBalance}`);
}, 1000);
}
app.post('/transfer', (req, res) => {
const amount = 50;
try {
transfer(amount);
res.send('Transfer completed');
} catch (error) {
res.status(400).send(error.message);
}
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
From your terminal -
curl -X POST http://localhost:3000/transfer & curl -X POST http://localhost:3000/transfer &
Did you notice? You still have 50$!
This is broken! If two requests run simultaneously, both read $100, both think there's enough, and both deduct $50 — ending up with a final balance of $50, when it should be $0.
Let's fix the problem -
const { Mutex } = require('async-mutex');
const mutex = new Mutex();
async function transfer(amount) {
const release = await mutex.acquire();
try {
const currentBalance = accountBalance;
if (currentBalance < amount) {
throw new Error('Insufficient funds');
}
await new Promise(resolve => setTimeout(resolve, 100));
accountBalance = currentBalance - amount;
console.log(`Transfer successful. New balance: $${accountBalance}`);
} finally {
release();
}
}
Now, only one transfer runs at a time. The second request waits until the first finishes. And you may have noticed - your balance is now zero.
This is a hypothetical example, but race conditions like this do happen in real-world applications — such as counting page views, managing product stock in e-commerce platforms, and more. The scary part? They often go unnoticed until they cause serious issues.