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.

Apr 21, 2025 - 16:44
 0
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:

  1. What a race condition is (in the context of banking)
  2. How to simulate one in Node.js
  3. 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.