Async Race Conditions

Posted in: JavaScript by Example (2 min read)

The term "race condition" is usually applied to the conflict in accessing shared variables in a multi-threading environment. In Javascript, your JS code is executed only by a single thread at a time, but it's still possible to create similar issues.

This is a common problem when people are just blindly making their functions async, without thinking about the consequences.

Let's take a very simple example - lazy-loading some kind of single-instance resource.

The synchronous version is simple:

let res;
function get_resource() {
    if(!res) res = init_resource();
    return res;
}

Asynchronous version:

let res;
async function get_resource() {
    if(!res) res = await init_resource();
    return res;
}

Imagine get_resource() being called in a web server, on every request. If enough time passes between the first and the second request, everything will work fine. But what happens if you get more requests, while the first one is still waiting for the resource?

This can lead to serious problems that are very hard to debug.

More examples

Here are some examples (from this HN thread):

Account balance:

    async function deduct(amt) {
        var balance = await getBalance();
        if (balance >= amt)
            return await setBalance(balance - amt);
    }

And more subtle example:

  async function totalSize(fol) {
    const files = await fol.getFiles();
    let totalSize = 0;
    await Promise.all(files.map(async file => {
      totalSize += await file.getSize();
    }));
    // totalSize is now way too small
    return totalSize;
  }

Possible solutions

The best way to avoid these types of problems is to avoid async functions where it's not absolutely necessary (see: Functional core, imperative shell).

If it's not possible, you may want to consider using Mutex (from Mutual Exclusion) - once someone acquires the lock, other requests will be blocked, until the original holder release the lock.

i.e. with async-mutex package our previous example may look like this:

let res;
async function get_resource() {
    await mutex.runExclusive(async () => {
        if(!res) res = await init_resource();
    });
    return res;
}

async-mutex has also support for semaphores - it's a similar concept, but multiple locks can be acquired.


Comments, suggestions, and questions are welcome.