DEV Community

Nick | OneThingWell.dev
Nick | OneThingWell.dev

Posted on • Edited on • Originally published at onethingwell.dev

Understanding Javascript Async Race Conditions

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;
}
Enter fullscreen mode Exit fullscreen mode

Asynchronous version:

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

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);
    }
Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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


Note: this is a snapshot of (WIP) topic from the Understanding Simplicity wiki. All suggestions (and reactions) are welcome. You can find the latest version here: Understanding Javascript Async Race Conditions

Top comments (1)

Collapse
 
pavelee profile image
Paweł Ciosek

thanks for tip with mutex!