John Skiles Skinner's portfolio

A picture of colorful television static

JS promises for faster code

In which I use jQuery .when.apply() to process the result of multiple API calls, and make code faster by avoiding setTimeout()

Note: code used below accomplishes things with jQuery that can also be done with ES6 if you are programming in an environment where you may assume your users' browsers are up to date enough to support the latest JavaScript.

The transformation

I am pretty proud of a function I refactored recently for my job at Cornell University Library. The diff can be found on GitHub, but just have a glance at how much the code changed:

refactor

This large reduction in number of lines of code was driven by re-thinking the technique for making multiple API requests.

Should we wait for requests to finish?

The function makes calls to three APIs, it does some processing of the results of the API calls, and then it passes the results to the next function. The processing of those API requests can't simply be coded in line after the API calls, because JavaScript will run the processing before the API calls are completed! Something must be done to ensure these things run in the right order.

The code (before refactoring) used the success and complete methods to sequence the processing after the API calls. Then, to pass control flow to the next function, it waited with the setTimeout() method. So, the code simply waited for a fixed time that, in the judgement of the developer, is probably long enough that most or all of the API calls have finished.

There is a big problem with this approach: the time waited might be wrong. If the time is too short, the API calls won't be complete when the code attempts to process them. If the time is too long, the code will be wastefully idle while the API calls have already finished. It would be nice to process the results when, and only when, they are actually ready!

Why not use callbacks?

Maybe the code could sequence things using a callback? After all, the success and complete methods are a kind of callback, right? Can't we just pass control flow to the next function by the same technique?

That would work if there were only one API call, but there are three. Whichever one passes the callback may not be the last one to execute. We need to wait for all of them.

Promises, promises

We can use promises to sequence our code. jQuery provides a tricky way to await not just one promise, but an indefinite number of them. It looks like $.when.apply($, ...) followed by .done( ... ).

The trick works because:

The below code shows how I used this technique:

function ajaxRequestsForSuggestedSearches(q) {
var ajaxParametersList = [
{
url: 'https://lookup.ld4l.org/authorities/search/...',
dataType: 'json'
},
{
url: 'http://lookup.dbpedia.org/api/search/...',
dataType: 'json'
},
{
url: 'https://www.wikidata.org/w/api.php?action=...',
dataType: 'jsonp'
}
];
// return an array of Ajax promises
return ajaxParametersList.map(p => $.ajax(p));
}

function gatherSuggestions(q) {
// get array of Ajax request promises
var ajaxRequests = ajaxRequestsForSuggestedSearches(q);
// run each request in the array
var whenRequests = $.when.apply($, ajaxRequests);
// when done running, process responses
whenRequests.done(function(ld4l, dbpedia, wikidata){

// ... do stuff to process request results ...

// pass the output of processing on to next function
checkSuggestions(processedData);
})
}

The first function in the above code starts with an array of info that will be used for network connections. The last line of that first function sets up an $.ajax() promise for each connection.

The second function in the code takes that array of promises and uses $.when to prepare to do further work when each promise is resolved. The method .done() is used to specify that further work.

I think it is bad/weird to put large chunks of code inside the level of indentation that .done() causes, so my program's control flow passes pretty quickly to another function (named checkSuggestions() in this case).

But, that's not the only reason to break your code into functions of manageable size! I've written more about that here


Random note: I experimented with processing the requests with the dataFilter option built in to &.ajax(), instead of processing them within done.

It turns out that dataFilter does not work for JSONP requests! Thanks to David Flanagan's JavaScript book for that insight!


Top of page | Blog index | Return home