Showing the Right Data with Out-of-Order Responses in Javascript

Javascript is an asynchronous language, which means sometimes things don't happen in the order you expect. This pattern ensures that the right response is shown.

Posted by Tejus Parikh on November 19, 2017

I recently added some advanced filtering to one of the screens in our AngularJS application. . Users could select checkboxes or values from a dropdown and the search results will automatically update. Everything worked great in development, but as soon as we pushed to our stating environment, our QA tests reported getting the incorrect results. A quick check of the network pane in Chrome showed the problem, our requests were coming back out-of-order.

Today's web programmers have so much choice in tools and frameworks that the choice itself causes angst. Still, unless you are using websockets, beneath all the layers of abstractions is the first dynamic web programming paradigm of Asynchronous Javascript and XML (AJAX). JSON may have replaced XML, but the asynchronous has remained unchanged. Hidden behind powerful frameworks, it can be easy to forget about this quirk of web-programming.

Google Chrome Network Panel Showing the Problem

The network panel clearly lays out the problem. The earliest returned last.

Asynchronous communication means that results can return out of order. We made it quick for users to change the filter, which also meant we made it likely that the earlier requests to team_index would come back after the later ones. The user interface was showing the current filters, but the results were from the filter that the user had selected before, creating a lot of confusion.

So one core feature of javascript was at fault, but two other core features of javascript, single-threadedness and function scopes, make a solution possible. We use AngularJS and ng-resource but this pattern can be applied universally.

The old code looked like this:

$ctrl.is_loading = false;

function load() {
    var params = $ctrl.filter_cmd.toMap();
    $ctrl.is_loading = true;
    User.team_index(params, function(response) {
        $ctrl.is_loading = false;
        $ctrl.result_list = response;
    });
}

This code is pretty straight forward. The state of the filter is stored in $ctrl.filter_cmd, whose properties are mapped to interface elements in an Angular component. $ctrl.is_loading gets flipped to true when there is a request, so that we know to show the user a loading indicator. When the response comes back, we update $ctrl.result_list and turn the loading indicator off.

What happens in the situation above is that two requests are ongoing at once. The last request (the one the users wants), returns first, the loading indicator is turned of and for a few milliseconds, the correct information is shown. But then the first request returns and $ctrl.result_list is overridden with the wrong information.

One way to solve the problem is to disable the UI elements while the page is loading. This solution is sub-optimal. A quick query might mean that the user's click gets blocked without a perceptible UI change, which breaks the principle of least surprise.

A better solution is to display the content that matches the users expectation. Thankfully this is easy to do.

The code sample above has been changed to the following:

var active_query = null;
$ctrl.isLoading = function() { return !!active_query;j };

function load() {
    var my_query = active_query = Math.random().toString(36) // It really doesn't matter what this is
    var params = $ctrl.filter_cmd.toMap();
    User.team_index(params, function(response) {
        if(active_query == my_query) {
            $ctrl.active_query = null;
            $ctrl.result_list = response;
        }
    });
}

Why Does This Work?

At first glance this could be a little confusing. Why are we checking the equality of something we defined only a few lines before?

The answer is in the two javascript features that were mentioned before. First, since javascript is single-threaded, the last call to the load() function is the one that will match the user's input.

The second feature is javascripts function scopes. There are three scopes in which variables are defined. There's the overall scope, where active_query is defined. There's the scope in load() where my_query is defined, and finally there's the scope in the success callback that has access to both. We can illustrate what's happening by adding a counter and a log statement like the following:

console.log("Request #: ", num_requests, ", my_query: ", my_query, ", active_query: ", active_query) 

We'll add a line before and after the call to User.team_index. If the first request takes longer to execute than the second, we'll get something like the following output:

Pre Request #: 1, my_query: foo, active_query: foo
Pre Request #: 2, my_query: bar, active_query: bar 
Post Request #: 2, my_query: bar, active_query: bar 
Post Request #: 1, my_query: foo, active_query: null

The if check will now fail and the user won't be presented with the wrong results.

Original image is CC-licensed [original source]

Related Posts:

Tejus Parikh

Tejus is the CTO and co-founder of WideAngle and writes weekly about building startups and the technology that powers them from Atlanta, GA, the startup capital of the south. Get my content on twitter, via RSS, or in your inbox:

comments powered by Disqus