A guide towards writing better AJAX calls in jQuery

For awhile, the standard way of making an AJAX call using jQuery is rather simple — by using the $.ajax() function:

$.ajax({
  data: someData,
  dataType: 'json',
  url: '/path/to/script',
  success: function(data, textStatus, jqXHR) {
// When AJAX call is successfuly
    console.log('AJAX call successful.');
    console.log(data);
  },
  error: function(jqXHR, textStatus, errorThrown) {
// When AJAX call has failed
    console.log('AJAX call failed.');
    console.log(textStatus + ': ' + errorThrown);
  },
  complete: function() {
// When AJAX call is complete, will fire upon success or when error is thrown
    console.log('AJAX call completed');
  };
});

Looks easy peasy, but there are several drawbacks to this method:

  1. Excessive nesting. Functions and events dependent on the returned AJAX data has to be wrapped in the success handler, because AJAX is by nature, asynchronous. Along the same line, this reduces the readability of your code.
  2. Difficulty in chaining and compounding. It is difficult and overwhelmingly complex to evaluate outcomes of multiple AJAX requests — we are unable to predict how long does it take for all AJAX requests (each running asynchronously) to return a response.
  3. Deprecation warning. As of v1.8 and above, jqXHR.success()errorand complete callbacks have been officially deprecated.

Promises and deferred objects

Thankfully, promises and deferred objects are implemented natively in the $.ajax() method:

  • .done() as a replacement for .success()
  • .fail() as a replacement for .error()
  • .always() as a replacement of .complete()

As per other native jQuery methods, you can chain them, i.e.:

$.ajax({
data: someData,
dataType: 'json',
url: '/path/to/script'
}).done(function(data) {
// If successful
console.log(data);
}).fail(function(jqXHR, textStatus, errorThrown) {
// If fail
console.log(textStatus + ': ' + errorThrown);
});

Chaining works, just as usual

Even better: assign the $.ajax() method to a variable, for which you can chain promise callbacks to. Chaining has always been a hallmark of jQuery, which allows one to reuse a cached selector, for example. In this case, the $.ajax() method returns a promise object natively, which we can use for chaining, too:

var ajaxCall = $.ajax({
context: $(element),
data: someData,
dataType: 'json',
url: '/path/to/script'
});

ajaxCall.done(function(data) {
console.log(data);
});

See that how the .done() callback is on the same level as the AJAX call itself, without requiring complicated nesting within the call per se? While this advantage may seem trival on a cursory glance, it greatly improves the readability of your code, especially in a production environment where your code is likely to be cluttered with multiple asynchronous calls.

It also allows you to inject code between the AJAX call itself and resolving the promise delivered by the call, allowing for greater flexibility in coding.

In other words, you can see that promises and deferred objects, implemented since jQuery v1.5 onwards, in relation to synchronicity of code execution, is akin to event bubbling in the DOM, listened on by the .on() method.


Multiple AJAX calls

Compounding several AJAX calls has never been made easier with promises. All you have to do is to listen to their status via $.when().

var a1 = $.ajax({...}),
a2 = $.ajax({...});

$.when(a1, a2).done(function(r1, r2) {
// Each returned resolve has the following structure:
// [data, textStatus, jqXHR]
// e.g. To access returned data, access the array at index 0
console.log(r1[0]);
console.log(r2[0]);
});

Dependence chain of AJAX requests

You can also chain multiple AJAX request — for example, when the second AJAX call relies on returned data on the first call. Let’s say the first call retrieves the session ID of a user, and we need to pass that value off to a second script. Remember that $.then() returns a new promise, which can be subsequently passed to the $.done() or even another $.then() method.

var a1 = $.ajax({
url: '/path/to/file',
dataType: 'json'
}),
a2 = a1.then(function(data) {
// .then() returns a new promise
return $.ajax({
url: '/path/to/another/file',
dataType: 'json',
data: data.sessionID
});
});

a2.done(function(data) {
console.log(data);
});

Modularizing AJAX requests

More often than not, one might want to modularize their code and delegate a single function to make dynamic AJAX requests. How should we invoke the AJAX call individually, and access the promise returned, in this case? It turns out to be beyond simple: simply return the AJAX object after you make a request.

Let’s say we want to make two AJAX calls, which basically uses the same parameters except for the data and url parameters:

// Generic function to make an AJAX call
var fetchData = function(query, dataURL) {
// Return the $.ajax promise
return $.ajax({
data: query,
dataType: 'json',
url: dataURL
});
}

// Make AJAX calls
// 1. Get customer order
// 2 Get customer ID
var getOrder = fetchData(
{
'hash': '2528ce2ed5ff3891c71a07448a3003e5',
'email': 'john.doe@gmail.com'
}, '/path/to/url/1'),
getCustomerID = fetchData(
{
'email': 'john.doe@gmail.com'
}, '/path/to/url/2');

// Use $.when to check if both AJAX calls are successful
$.when(getOrder, getCustomerID).then(function(order, customer) {
console.log(order.data);
console.log(customer.data);
});

No more messy nesting, and no more the need to repeat the $.ajax()method over and over again.


Handling arrays of returned deferred objects

Sometimes you would want to use $.when() on an array of deferred objects. An example situation would be: making a series of AJAX calls (number of calls dynamically changes, perhaps?), and then checking when all of them are done. How does that work?

There are two options:

  1. The easier to understand method would be to construct an empty array, use $.each() to make AJAX calls iteratively and then push the returned promise into the array.
  2. The preferred method (personally) would be to use .map() to construct an object containing returned promises, which we then use .get()to return an array

After that it’s all easy: simply use $.when.apply($, array) to evaluate all AJAX calls performed:

// Let's say we have a click handler and fires off a series of AJAX request
$selector.on('click', function() {
// Construct empty array
var deferreds = [];

// Loop using .each
$(this).find('div').each(function() {
var ajax = $.ajax({
url: $(this).data('ajax-url'),
method: 'get'
});

// Push promise to 'deferreds' array
deferreds.push(ajax);
});

// Use .apply onto array from deferreds
$.when.apply($, deferreds).then(function() {
// Things to do when all is done
});
});
// Let's say we have a click handler and fires off a series of AJAX request
$selector.on('click', function() {
// Map returned deferred objects
var deferreds = $(this).find('div').map(function() {
var ajax = $.ajax({
url: $(this).data('ajax-url'),
method: 'get'
});

return ajax;
});

// Use .apply onto array from deferreds
// Remember to use .get()
$.when.apply($, deferreds.get()).then(function() {
// Things to do when all is done
});
});

You can see that using promises and deferred object offers an unparalleled advantage over nesting jqXHR.success callbacks when making iterative AJAX calls that should be evaluated as a whole.


Final notes

Of course, the aforementioned promises and deferred objects can also be used for $.get() and $.post() methods, which are basically reduced shorthand functions for the $.ajax() method using the GET or POST methods.