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.