jQuery Deferred Objects – Part 2

December 12, 2011 5 Comments by Robin

Deferred Objects – Part II

As you recall from the first part jQuery Deferred Objects – Part I, deferred objects give you a way to organize asynchronous blocks of code in to a self-managed callback queue, while allowing you to attach 1..* callbacks. Also, you’ll recall that the basics of using jQuery deferred objects include essentially the following “simplified” steps:
1. Created a deferred object
2. Attached .done and .fail callbacks
3. Called resolve or reject, thereby triggering the appropriate respective callbacks

Promises

This was fine as a first step in understanding what’s going on, but in all likelyhood you’ll use a slightly different approach. What’s wrong with returning a deferred object itself? It gives the caller access to resolve or reject directly, and that means other callers could be affected as a result. What we really want to do is return a “promise”. From the jQuery Promise Documentation we learn:
“This object provides a subset of the methods of the Deferred object (then, done, fail, always, pipe. isResolved, and isRejected) to prevent users from changing the state of the Deferred.”

$.when(
    $.ajax( "/push_to_twitter" ),
    $.ajax( "/push_to_facebook" ),
    $.ajax( "/push_to_linkedin" ),
    $.ajax( "/generate_report" )
).then( it_succeeded, it_failed);
 
function it_succeeded(report_data){
    // Now do something with the report data e.g. show
    // an awesome summary with pretty bars and graphs!
}    
function it_failed(){
    // Treat user's depression, OCD and other ailments
    // Prescribe prozac, etc., etc...
}

Possible Uses

Possible use cases are endless but here are some examples:
1. You need to make multiple asynchronous Ajax requests – with Deferred objects you can handle the results together without concern for which order they finish in.
2. You want to be able to make the call in one region of code, then pass the result as an argument to a different part, but the call may be asynchronous.
3. Easily return data from a modal dialog
4. Caching – an API properly supporting Deferred objects can implement caching without any changes to the consumer of the API.

An Example: Caching

For caching, jQuery’s when .. then provides us the tools we need to return both fresh and cached data from one interface. Take the following code:

var cacheA = {};
function fetch_some_objectA(key){
    if (cacheA[key]) { return cacheA[key]; }
    return cacheA[key] = $.ajax('/fooEndpoint', {
        // ajax options here
    }).done(function(res){
        cacheA[key] = res;
    });
}
var cacheB = {};
function fetch_some_objectB(key){
    if (cacheB[key]) { return cacheB[key]; }
    return cacheB[key] = $.ajax('/barEndpoint', {
        // ajax options here
    }).done(function(res){
        cacheB[key] = res;
    });
}
 
$.when(fetch_some_objectA('foo'), fetch_some_objectB('bar').done(function(dataA, dataB){
    // Do whatever you want with said data
}).fail(function(errA, errB) {
    // Deal with error handling
});

The basics of caching with Deferred objects

There are a couple of cases to consider here; the first case is that you are requesting object(‘foo’) for the first time. In this case, cacheA[“foo”] will not exist and so an ajax call is created. $.ajax returns a deferred object, which is saved in the cache. The reason this is important is that thereafter, future requests will get that same deferred object, thus preventing two requests from being made to the same endpoint. Once the ajax call completes the done handler we set updates the cache with the actual object. Any calls that ran fetchsomeobjectA(‘foo’) while the request was “in progress” will be notified with the data once the deferred is resolved. Calls made after the request has resolved will get the actual data straight away. So, how do you tell if you have a deferred or the real data? The answer is simple: you don’t need to.

This is where jQuery.when neatly ties everything together. jQuery.when ($.when) accepts any number of arguments and returns a jquery.Deferred object. That object will be resolved once all of the arguments are resolved; however, if those objects are not jQuery.Deferred objects then they will be considered resolved immediately. From the documentation, jQuery.when takes either a Deferred or object: “If a single argument is passed to jQuery.when and it is not a Deferred, it will be treated as a resolved Deferred and any doneCallbacks attached will be executed immediately.”

After passing through a jQuery.when, a Deferred object that resolves to a string “A” and the string “A” become functionally equivalent — no matter which you passed in, you’ll still get the same thing:

$.when("fdsafdsa").done(function(var) { alert(var); });

This will immediately alert “fdsafdsa”. Now essentially the same using the deferred resolve idiom:

var dfd = new $.Deferred();
$.when(dfd).done(function(var) { alert(var); });
dfd.resolve("fdsafdsa");

This will also alert the same thing; it doesn’t matter if it’s a real object or a deferred, once you make it into the when it’ll be a real object. This makes caching really slick because you don’t need to care if you have the object yet or not, you just let jQuery.when figure that out and tell it what should happen once it does.

One small problem…

If you actually copy in this code and play with it long enough, you’ll notice something that doesn’t quite work how you expect. When the jQuery.ajax call returns you’ll actually get a slightly different object in the jQuery.when callback; this actually makes sense if you look at it closer. From the jQuery codebase, when the ajax call completes it is either resolved or rejected with the following lines:

// Success/Error
if ( isSuccess ) {
    deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
} else {
    deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
}

Notice that there are actually three arguments in either case; this may surprise you, since most examples you’ll see of the callbacks accept just one, like the above code. Most don’t need the other two, but jQuery.when isn’t going to discard those extra values and yet it still needs to keep one argument per input in the callback function. Therefore, in the $.when block above you’ll actually have both dataA and dataB resolve to an array with three values (success, statusText, jqXHR); however, we normally would only cache one of those values (success) so when reading from the cache we don’t get that array. The solution is to make our functions a little bit more complicated by using the jquery pipe method to restructure the result a little bit:

var cacheA = {};
function fetch_some_objectA(key){
    if (cacheA[key]) { return cacheA[key]; }
    return cacheA[key] = $.ajax('/fooEndpoint', {
        // ajax options here
    }).pipe(function(res){
        return cacheA[key] = res;
    });
}
var cacheB = {};
function fetch_some_objectB(key){
    if (cacheB[key]) { return cacheB[key]; }
    return cacheB[key] = $.ajax('/barEndpoint', {
        // ajax options here
    }).pipe(function(res){
        return cacheB[key] = res;
    });
}
 
$.when(fetch_some_objectA('foo'), fetch_some_objectB('bar').done(function(dataA, dataB){
    // Do whatever you want with said data
}).fail(function(errA, errB) {
    // Deal with error handling
});

You may notice the assignment of the result to the cache happens twice. The reason is that you first want to assign the deferred object to the cache when you start the request — otherwise if you call the function again before the ajax request finishes it’ll start a new request rather than reusing the old one.

With this new object (the result of the .pipe) the resolved value will be as we expect it. This relatively simple pattern can get a little more complex if you’re doing a lot with pipe, but for the consumer the interface is really simple.

Admittedly, there is some complexity to using this pattern but the “win” is that your interface now has a “Single Point of Contact” (SPOC)
Let’s look at a convenient analogy…

An Example: Placing a Special Order

Let’s imagine that, as an active supporter of your local community businesses, you make it a point to go to your favorite little “mom and pop” bookstore to place orders for books. The lady at the counter asks for your phone number or email, and says she’ll contact you as soon as your books come in. Now obviously, there’s the small chance that, after the fact, the wholesaler, manufacturer, etc., will have ran out of the ordered book(s). Should this happen, you’d probably feel it was acceptable for the bookstore to contact you and give you the option to wait or cancel your order. However, you would not want to here directly from the wholesaler or manufacturer directly. This would probably confuse you and you may even complain to the bookstore that they should manage their purchases better should this happen! The expectation you have as a customer in this example is that you only have to deal with a single point of contact. In code, we can have a similar convenience using deferred objects:

// Contrived ajax and random failure stubs so we can
// pretend to have "mostly successful" requests
function randomTrueFalse() {
    Math.random();
    var r = Math.round(Math.random()*10);
    return (r%9 !== 0);
};
 
function ajaxStub(endpoint) {
    var dfdResult = new $.Deferred();
    var bool = randomTrueFalse();
 
    setTimeout(function() {
        if(bool) {
            dfdResult.resolve('success: '+endpoint);
        }
        else {
            dfdResult.reject('fail: '+endpoint);
        }
    }, 100);
    return dfdResult.promise();
};
 
var manufacturer = (function() {
    return {
 
        orderBook: function(bookName) {
            return $.when(ajaxStub('/book/'+bookName)).pipe(function(respObj) {
                console.log("Manufacturer-DONE: ", respObj);
                return respObj;
            }, function(errObj) { console.log("Manufacturer-FAIL: ", errObj); return errObj; });
        }
    };
}());
 
var wholesaler = (function() {
    var m = manufacturer;
    return {
        orderBook: function(bookName) {
            return $.when(m.orderBook(bookName)).pipe(function(respObj) {
                console.log("Wholesaler-DONE: ", respObj);
                return respObj;
            }, function(errObj) { console.log("Wholesaler-FAIL: ", errObj); return errObj; });
        }
    };
}());
 
var retailer = (function() {
    var w = wholesaler;
    return {
        orderBook: function(bookName) {
            return $.when(w.orderBook(bookName)).pipe(function(respObj) {
                console.log("Retailer-DONE: ", respObj);
                dfdResult.resolve(respObj);
            }, function(errObj) { console.log("Retailer-FAIL: ", errObj); return errObj; });
        }
    };
}());
 
// Notice that, although we have multiple potential points of failure above (e.g.
// Manufacturer, Wholesaler, Retailer, the customer only needs to deal with the
// retailer directly.)
function makePurchase() {
 
    // Place an order for two books
    for(var i=1; i<3; i++) {
        $.when(retailer.orderBook("Great Book - Part " + i)).done(function(respObj) {
            console.log("Purchaser-DONE... respObj: ", respObj);
        }).fail(function(err) {
            console.log("Purchaser-FAIL... err: ", err);
        });
    }
};
makePurchase();
 
/*
Random output for placing order of two books might look like:
Manufacturer-FAIL: fail: /book/Great Book - Part 1
Wholesaler-FAIL: fail: /book/Great Book - Part 1
Retailer-FAIL: fail: /book/Great Book - Part 1
Purchaser-FAIL... err: fail: /book/Great Book - Part 1
Manufacturer-DONE: success: /book/Great Book - Part 2
Wholesaler-DONE: success: /book/Great Book - Part 2
Retailer-DONE: success: /book/Great Book - Part 2
Purchaser-DONE... respObj: success: /book/Great Book - Part 2
*/

Start at the bottom at the makePurchase() call and you should see that we’ve essentially modeled our story of a customer (purchaser) placing an order for two books. Should the purchase succeed or fail, the purchaser will only ever have to deal with the retailer directly (as expected), because the fact that deferred promises can sort of “bubble back up” the call chain. This keeps the interface quite clean, and the caller’s required knowledge of inner working to a minimum.

Conclusion

I’ve purposely kept the examples contrived so you can focus on the principles of deferred objects. If you’d like to see a variety of other example implementations, Julian Aubourg and Addy Osmani have written a wonderful article you should definitey have a look at. jQuery deferred objects take a bit to get used to, but provide some very compelling advantages for web applications that require heavy ajax or graphics.