javascriptasynchronousasync-awaitevent-loop

Does this JavaScript example create “race conditions”? (To the extent that they can exist in JavaScript)


I am aware JavaScript is single-threaded and technically can’t have race conditions, but it supposedly can have some uncertainty because of async and the event loop. Here’s an oversimplified example:

class TestClass {
  // ...

  async a(returnsValue) {
     this.value = await returnsValue()
  }
  b() {
     this.value.mutatingMethod()
     return this.value
  }
  async c(val) {
     await this.a(val)
     // do more stuff
     await otherFunction(this.b())
  }
}

Assume that b() relies on this.value not having been changed since the call to a(), and c(val) is being called many times in quick succession from multiple different places in the program. Could this create a data race where this.value changes between calls to a() and b()?

For reference, I have preemptively fixed my issue with a mutex, but I’ve been questioning whether there was an issue to begin with.


Solution

  • Yes, race conditions can and do occur in JS as well. Just because it is single-threaded it doesn't mean race conditions can't happen (although they are rarer). JavaScript indeed is single-threaded but it is also asynchronous: a logical sequence of instructions is often divided into smaller chunks executed at different times. This makes interleaving possible, and hence race conditions arise.


    For the simple example consider...

    var x = 1;
    
    async function foo() {
        var y = x;
        await delay(100); // whatever async here
        x = y+1;
    }
    

    ...which is the classical example of the non-atomic increment adapted to JavaScript's asynchronous world.

    Now compare the following "parallel" execution:

    await Promise.all([foo(), foo(), foo()]);
    console.log(x);  // prints 2
    

    ...with the "sequential" one:

    await foo();
    await foo();
    await foo();
    console.log(x);  // prints 4
    

    Note that the results are different, i.e. foo() is not "async safe".


    Even in JS you sometimes have to use "async mutexes". And your example might be one of those situations, depending on what happens in between (e.g. if some asynchronous call occurs). Without an asynchronous call in do more stuff it looks like mutation occurs in a single block of code (bounded by asynchronous calls, but no asynchronous call inside to allow interleaving), and should be OK I think. Note that in your example the assignment in a is after await, while b is called before the final await.