Friday, March 29, 2024

jQuery Promises & Deferred: How to Use Them

5/28/13

Promises are like babies: easy to make, hard to deliver. ~Author Unknown

Remember when you wanted to associate an event handler to a mouseclick, you would assign it to the element’s onclick event, as in mywidget.onclick = myhandler;. This became problematic when another method also wanted to get in on the click action since you could only assign one function at a time. Eventually, the issue was solved by the addEventListener() DOM function, which allowed you to add as many listeners as you wanted. Fast forward to the present, and a similar problem has emerged with Ajax calls. This time it’s Ajax’s limitation of only supporting one callback function. jQuery introduced the Deferred object in version 1.5 to solve this problem. It can register multiple callbacks into callback queues, invoke callback queues, and relay the success or failure state of any synchronous or asynchronous function. In today’s article, we’ll learn how to use the Deferred object with Promises.

What’s In a Promise?

Prior to jQuery 1.5, a typical Ajax call looked like this:

$.ajax({
  url: "/ServerResource.txt",
  success: successFunction,
  error: errorFunction
});

Since then, Ajax calls’ return object (jQuery XMLHttpRequest (jqXHR)) implements the CommonJS Promises/A interface, which offers greater flexibility and, hopefully, consistency across libraries.

var promise = $.ajax({
  url: "/ServerResource.txt"
});
 
promise.done(successFunction);
promise.fail(errorFunction);
promise.always(alwaysFunction);

The always() handler, referred to as complete() before jQuery 1.6, executes after the done() or fail() events, regardless of the Ajax call result.

All three done(), fail(), and always() handlers return the same jQuery XMLHttpRequest (jqXHR) object, so it is perfectly acceptable to chain them together:

$.ajax( "example.php" )    
    .done(function() { alert("success"); })    
    .fail(function() { alert("error"); })    
    .always(function() { alert("complete"); }); 

However, you will have to store the jqXHR object to a variable if you need to refer to it further down the line:

var jqxhr = $.ajax( "example.php" )    
    .done(function() { alert("success"); })    
    .fail(function() { alert("error"); })    
    .always(function() { alert("complete"); }); 
    
    // perform some work here ... 
    
    // Set another completion function for the request above
    jqxhr.always(function() { alert("another complete"); });

Another way to combine the handlers is to use the then() method of the Promise interface. It accepts all three handlers as arguments. With regards to jQuery, prior to version 1.8, you could pass an array of functions to the then() method:

$.ajax({url: "/ServerResource.txt"}).then([successFunction1, successFunction2, successFunction3], 
                                          [errorFunction1, errorFunction2]);

//same as

var jqxhr = $.ajax({
  url: "/ServerResource.txt"
});
 
jqxhr.done(successFunction1);
jqxhr.done(successFunction2);
jqxhr.done(successFunction3);
jqxhr.fail(errorFunction1);
jqxhr.fail(errorFunction2);

Since 1.8, the then() method returns a new promise that can filter the status and values of a deferred through a function, replacing the now-deprecated deferred.pipe() method. For all signatures, the arguments can be set to null if you don’t want to assign a handler for that event type. Best of all, you can rest assured that callbacks are guaranteed to run in the order they were passed in.

var promise = $.ajax({
  url: "/ServerResource.txt"
});
 
promise.then(successFunction, errorFunction);

 

var promise = $.ajax({
  url: "/ServerResource.txt"
});

promise.then(successFunction); //no handler for the fail() event

Chaining then() Functions

Then functions can be chained together to call multiple functions in succession:

var promise = $.ajax("/myServerScript1");
 
function getStuff() {
    return $.ajax("/myServerScript2");
}
 
promise.then(getStuff).then(function(myServerScript2Data){
  // Do something with myServerScript2Data
});

Combining Promises

The Promise $.when() method is the equivalent of the logical AND operator. Given a list of Promises, when() returns a new Promise whereby:

  • When all of the given Promises are resolved, the new Promise is resolved.
  • When any of the given Promises is rejected, the new Promise is rejected.

The following code uses the when() method to make two simultaneous Ajax calls and execute a function when both have successfully finished:

var jqxhr1 = $.ajax("/ServerResource1.txt");
var jqxhr2 = $.ajax("/ServerResource2.txt");
 
$.when(jqxhr1, jqxhr2).done(function(jqxhr1, jqxhr2) {
  // Handle both XHR objects
  alert("all complete");
});

Promise States

At any moment in time, promises can be in one of three states: unfulfilled, resolved or rejected. The default state of a promise is unresolved. Any handlers set are queued to be executed later. For instance, if an Ajax call is successful then $.resolved is called, its state is set to resolved and any “done” handlers set are executed. Should the call fail, $.rejected is called, its state is set to rejected and any “fail” handlers set are executed.

Making Your Own Deferred Process

By creating a new Deferred object via the jQuery.Deferred() method, we can setup our own deferred processes. In the following example, a <DIV> or <SPAN> element is updated based on the state of our process. The setInterval() method starts the process in motion, while setTimeout() determines when the process should end. Inside the process() method, a Deferred object is assigned to a local variable so that we can call its resolve(), promise(), and notify() events:

var timer;
$('#result').html('waiting…');
 
var promise = process();
promise.done(function() {
  $('#result').html('done.');
});
promise.progress(function() {
  $('#result').html($('#result').html() + '.');
});

function process() {
  var deferred = $.Deferred();

  timer = setInterval(function() {
    deferred.notify();
  }, 1000);
  
  setTimeout(function() {
     clearInterval(timer);
     deferred.resolve();
  }, 10000);
  
  return deferred.promise();
}

It can also be written using the then() method:

var timer;

(function process() {
  $('#result').html('waiting…');
  var deferred = $.Deferred();
    
  timer = setInterval(function() {
    deferred.notify();
  }, 1000);
  
  setTimeout(function() {
     clearInterval(timer);
     deferred.resolve();
  }, 10000);
  
  return deferred.promise();
})().then(function() { $('#result').html('done.'); },
          null,
          function() { $('#result').html($('#result').html() + '.'); });

Conclusion

Promises are definitely the new way to go when handling asynchronous processes. Forget about traditional callbacks and give the Promises/A API a try. It’s a big step in the right direction.

Rob Gravelle
Rob Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured