How to avoid async race conditions in JavaScript

Slava Shpitalny
5 min readJan 17, 2017

--

You can have a race on a single thread…

Background…

I started my programming carrier as a C# programmer. I was part of a back-end project that had to perform well under a load of many requests while most of the handling is IO on large files, which can be slow. We used tasks to achieve the needed performance and had to deal with many asynchronous logic, locks, managing states and so on.

After a few years, I switched to developing web apps. I started with TypeScript since it was an easy switch from C# but had a few pure JavaScript projects.

I remember my first time using promises and understanding that even though the code is asynchronous, you don’t have to use locks to handle multi-threaded access to data. It was an amazing feeling, I don’t need to lock stuff, to check and switch to the UI thread to change UI stuff, JavaScript is single threaded, it was awesome!

But because of my past, I felt cheated, there must be something I am missing, it can’t be so simple. I looked on how the async stuff works in JavaScript world and found out about the message queues. Message queues are used to communicate between background workers and the same queues are used to continue async code after async request. The idea is that JavaScript is singe threaded (unless we use background workers), and when some async call returns, its result is put on a message queue, when the main thread finishes its work and free (no context switching, full finish), it takes the next message to handle it.

Once again, I felt blessed. I don’t need to handle async stuff. I can simply write:

fetch(url)
.then(data => data.json())
.then(items => updateState(items));

But! (There is always a but)

The problem…

Let us think for a second, when does this fetch call gets invoked?

  • Every time a user is clicking a button?
  • Every X seconds?
  • Every X pixels the mouse moves over a map?

How long does it take for the answer to return in the worst case? 1,2,10 seconds? Maybe more on a very slow network?

Let’s assume we have a scenario like this one:

  • We have a map
  • We want to show some information related to the location of the user
  • Every time a user is moving on the map we fetch for the data
  • We are smart and we even debounce the fetches for 0.5 seconds
  • The data is returned after 5 seconds (we are on a slow network)

Now let’s think of the next case:

  • The user opens the map
  • The first fetch request is fired
  • After 2 seconds the user moves on the map again because he felt like he found something interesting
  • After another 0.5 seconds, another fetch is fired

Now, if for some reason for the first fetch it took 5 seconds to return and for the second fetch it took 1 second to return (the network signal got better or the fetch/computation size is much smaller or we got not so busy server for the second time), what will happen now?

Well it depends, one thing we know for sure, the updateState will be called twice, first time with the second response, and the second time with the first response.

What will the final state of the application be? In a good scenario, we accumulate the responses and it is actually valid to show both results to the user. In another good scenario, we use some caching and both responses will be cached and the map will show only the data of the viewed area.

The worst case (and probably the real one) is if on each updateState call we delete the current state and set the new state. In this case what will happen is some weird async bug, we look at area2 but see the items of area1.

My point is even though you don’t need to manage async code using locks, you still need to think about async problems. In this case a race condition. When I see JavaScript code, it usually doesn’t handle those.

Some solutions…

So you ask me what can I do?

  • You can save the last request and cancel it on the next one. For the moment of writing this article, fetch doesn’t have a cancellation API (https://github.com/whatwg/fetch/issues/27). But for the sake of the argument here is a code with setTimeout:
if (this.lastRequest) {
clearTimeout(this.lastRequest);
}
this.lastRequest = setTimeout(() => {
updateState([1,2,3]);
this.lastRequest = null;
}, 5000);
  • You can create a session object on every new request and save it, and on response check the saved one is still the same as the one you have:
const currentSession ={};
this.lastSession = currentSession;
fetch(url)
.then(data => data.json())
.then(items =>{
if (this.lastSession !== currentSession) {
return;
}
updateState(items);
}).catch(error =>{
if (this.lastSession !== currentSession){
return;
}
setError(error);
});

Here the currentSession object of each request is saved on the request closure and the lastSession is saved on this. Don’t forget the error handling, you don’t want to show an error when there is none.

Even though both methods take care of the race condition, in the first method (cancellation) there is still a case where there will be 2 responses returned:

  • First request is on the way
  • Second request is in the process, the if statement is evaluated
  • Now the first response returned and is put to the message queue
  • The second request is fired

Now the first and the second requests will be handled is the same order they were fired. This may still cause bugs. For example, if in the updateState method you set the isLoading indicator to false, so they user will get the items from the first request and after a while without any warning the other items will appear.

This is why usually it is better to combine both.

  • Cancel the previous unneeded request: it will free up the server of handling the unneeded request
  • In a case that only the last request is needed, save a session object and handle only the request with the correct session.

Summary…

This article is not about giving you the best solution for the async race condition, it is not even about the race condition, I write this article to make you aware of async problems that may occur even though JavaScript is single threaded, things you need to think about when working with async stuff.

There is not always a problem, sometimes the problem exists but it is reproduced in such rare conditions that you don’t really care or the fix costs too much and gives almost nothing, but you need to be aware of the problems you have in order to make the right decision.

Hope I helped at least someone… =]

--

--