Can node.js code result in race conditions?

Yes, race conditions (in the sense of a shared resource having an inconsistent value due to order of events) can still happen anywhere that there’s a point of suspension that could lead to other code being run (with threads its at any line), take for example this piece of async code that is entirely single threaded:

var accountBalance = 0;

async function getAccountBalance() {
    // Suppose this was asynchronously from a database or something
    return accountBalance;
};

async function setAccountBalance(value) {
    // Suppose this was asynchronously from a database or something
    accountBalance = value;
};

async function increment(value, incr) {
    return value + incr;
};

async function add$50() {
    var balance, newBalance;
    balance = await getAccountBalance();
    newBalance = await increment(balance, 50);
    await setAccountBalance(newBalance);
};

async function main() {
    var transaction1, transaction2;
    transaction1 = add$50();
    transaction2 = add$50();
    await transaction1;
    await transaction2;
    console.log('$' + await getAccountBalance());
    // Can print either $50 or $100
    // which it prints is dependent on what order
    // things arrived on the message queue, for this very simple
    // dummy implementation it actually prints $50 because
    // all values are added to the message queue immediately
    // so it actually alternates between the two async functions
};

main();

This code has suspension points at every single await and as such could context switch between the two functions at a bad time producing “$50” rather than the expected “$100”, this is essentially the same example as Wikipedia’s example for Race Conditions in threads but with explicit points of suspension/re-entry.

Just like threads though you can solve such race conditions with things like a Lock (aka mutex). So we could prevent the above race condition in the same way as threads:

var accountBalance = 0;

class Lock {
    constructor() {
        this._locked = false;
        this._waiting = [];
    }

    lock() {
        var unlock = () => {
            var nextResolve;
            if (this._waiting.length > 0) {
                nextResolve = this._waiting.pop(0);
                nextResolve(unlock);
            } else {
                this._locked = false;
            }
        };
        if (this._locked) {
            return new Promise((resolve) => {
                this._waiting.push(resolve);
            });
        } else {
            this._locked = true;
            return new Promise((resolve) => {
                resolve(unlock);
            });
        }
    }
}

var account = new Lock();

 async function getAccountBalance() {
    // Suppose this was asynchronously from a database or something
    return accountBalance;
};

async function setAccountBalance(value) {
    // Suppose this was asynchronously from a database or something
    accountBalance = value;
};

async function increment(value, incr) {
    return value + incr;
};

async function add$50() {
    var unlock, balance, newBalance;

    unlock = await account.lock();

    balance = await getAccountBalance();
    newBalance = await increment(balance, 50);
    await setAccountBalance(newBalance);

    await unlock();
};

async function main() {
    var transaction1, transaction2;
    transaction1 = add$50();
    transaction2 = add$50();
    await transaction1;
    await transaction2;
    console.log('$' + await getAccountBalance()); // Now will always be $100 regardless
};

main();

Leave a Comment