I saw a blog post stating that one of the advantages of node.js is
… since node.js is single-threaded, there are no chances of a race condition occurring…
And that seemed like an interesting thing to me. Let’s see if we can introduce a race condition into our node.js code.
The Problem
Let’s try to create a function that only calls another function once.
For the purposes of this example, let’s say we want to create a function incrementOnce
that calls another function increment
at most one time.
Starting simple
Let’s start with our increment
function (Let’s assume it’s an async function, since in a real application this would do some IO)
// script.js
let count = 0;
async function increment() {
count += 1;
console.log("count = ", count);
}
increment();
increment();
increment();
console.log("Final count = ", count);
This should output
$ node script.js
count = 1
count = 2
count = 3
Final count = 3
Which is exactly what we’d expect here
Adding our other function
So far so good, but let’s try creating an incrementOnce
function that calls our increment
function
// script.js
let count = 0;
async function increment() {
count += 1;
}
let called = false;
async function incrementOnce() {
if (!called) {
await increment();
called = true;
}
console.log("count = ", count);
}
incrementOnce();
incrementOnce();
incrementOnce();
console.log("Final count = ", count);
and running it now, we get
$ node script.js
count = 3
count = 3
count = 3
Final count = 3
That doesn’t look like what we wanted. incrementOnce
was supposed to call increment once, but it actually ended up calling it 3 times!
What’s going on?
So we use a called
flag to make sure that the count
is not incremented multiple times. Not only did this not work, the value is 3
in all 3
function calls instead of being 1
, 2
and 3
which is even weirder.
The reason for this is that when we await
an expression, we allow another function to execute in the meanwhile.
In our case we’re checking if the called
flag is set and calling increment
.
However we’re calling await
on increment
, which means other calls to incrementOnce
can start executing before the first increment
call is complete.
Which means that in this case, each call to incrementOnce
sees that called === false
, so all of them end going inside the if block.
By the time the called
variable is actually set to true
in the first call, the other calls have already finished checking the called
variable, and they each call the increment
function
So when we log the values, all 3 increments have been completed, so they all see the final value of count
which is 3
The easy fix
So how do we fix this? The easiest fix for our current snippet is to make sure we update our called
flag before we allow the function to be “suspended”, i.e. before await
// script.js
let count = 0;
async function increment() {
count += 1;
}
let called = false;
async function incrementOnce() {
if (!called) {
// Make sure we do this before any awaits
called = true;
await increment();
}
console.log("count = ", count);
}
incrementOnce();
incrementOnce();
incrementOnce();
console.log("Final count = ", count);
and running it again, we get
$ node script.js
count = 1
count = 1
count = 1
Final count = 1
Voila, it’s fixed
This example may be a bit simplified, but in real life, it’s not unusual to do a few async calls which update the same object. In those cases, if we want to make sure only one update happens we need to make sure we use guards properly.