Using Deferred Objects In jQuery 1.5 To Normalize API Responses
As many of you probably know, jQuery 1.5 was released at the end of January. A big part of this release was the addition of Deferred objects and a complete rewrite of the AJAX functionality which now relies heavily on deferred objects. As I mentioned on the ColdFusion Panel earlier this week, I don't really have a good understanding of Deferred objects; so, I thought I needed to take some time to explore what they are and how they work. After reading Eric Hynds overview of Deferreds, I thought a good place to start would be with the standardization of API responses.
Since I am just looking into this for the first time, it would be foolish of me to try and explain to you what a Deferred object is exactly. But, from what I can gather so far, it's a proxy object that can queue "success" and "fail" event handlers. The main "bind" methods are:
- Deferred.done( handler | [handler] )
- Deferred.fail( handler | [handler] )
- Deferred.then( done, fail )
In this API, the easy mental model is that done() maps to "success" and fail() maps to "failure." The then() method is simply a short-hand for setting both "done" and "fail" handlers at the same time (much like hover() is a short-hand for "mouseenter" and "mouseleave"). These binding methods can be called on the same deferred object multiple times; each subsequent call adds the given handler(s) to the first-in-first-out internal queue.
Once a Deferred object is setup, its state has to be changed before any event handlers will be executed. To trigger the success handlers, the deferred object has to be "resolved." To trigger the failure handlers, the deferred object has to be "rejected."
To explore the use of deferreds, I wanted to see if I could standardize the way AJAX responses are handled within my applications. With jQuery 1.5, the $.ajax() method now returns a specialized deferred object - jqXHR. The standardization of the response object API as a deferred object makes it much easier to proxy. As such, I want to see if I can use my own Deferred object to proxy the implicit deferred AJAX response.
The reason that this would be cool is that the AJAX functionality in the jQuery library depends on HTTP Status Codes. That is, only 20x status codes are considered to be "successful" requests. Everything else is considered a "failure." The problem with this is that if your ColdFusion application uses status codes to indicate API errors, jQuery won't parse the response data.
Ideally, it would be great if both the "success" and "failure" AJAX event handlers could receive data in a normalized, unified manner. Then, only success handlers would deal with truly successful API request; and, failure handlers would deal with everything else - but, without having any additional overhead.
To see what I'm talking about, take a look at the following API page. When you look through the code, notice that the API can return the following status codes:
- 200 OK
- 400 Bad Request
- 401 Unauthorized
- 500 Internal Server Error
Ok, let's take a look at the ColdFusion code.
When it comes to unexpected API errors, two things can happen: either the API explicitly returns a 500 response; or, the ColdFusion server messes up somewhere outside the API workflow and returns a ColdFusion error.
NOTE: By default, ColdFusion errors return a 500 status code response; however, if people use the onError() application event handler and don't explicitly set a response status code, it will be returned as 200 OK.
For our purposes, we won't worry about parsing 500 responses; however, you can see that everything in the 40x response code range will return a valid API response object. What I'm going to do now, with Deferred objects, is normalize the $.ajax() promise in such a way that 40x responses get parsed and handed off to the appropriate event handlers.
When looking at the following code, note that the $.ajax() response - request - gets passed to the normalizeAJAXResponse() before it is returned to the calling context.
By default, the $.ajax() "promise" (a read-only deferred object) won't parse the 40x responses. As such, standard fail() bindings won't receive valid API response objects. To get around this, I am creating a proxy Deferred object that uses the $.ajax() promise in order to afford a more normalized response value. Since the AJAX promise and my normalized promise present the same promise API, the calling context can treat my proxy deferred like it would the native AJAX deferred.
NOTE: My proxy deferred object does not provide AJAX-specific methods (ex. abort()); but, it could be expanded to do so.
Before the introduction of Deferred objects to the jQuery API, normalizing an $.ajax() request would not have been a trivial task; but, now that the $.ajax() method returns a "promise" (a read-only deferred object), creating proxy responses becomes much easier. In this case, I'm explicitly creating the proxy promise; however, I wonder if a scenario like this would lend well to the new jQuery.sub() method? Clearly, I'm still getting my feet wet with this concept; but, it looks like Deferred objects might be pretty cool after all.
Want to use code from this post? Check out the license.
Excellent blog. Best part being, covers all the new features of $.Deferred(). Bookmarked as a reference article. Thanks.
Thanks my friend - I'm happy this is making some sense; I'm still exploring myself.
Nice article, I'm looking forward to playing around with deferreds. jQuery has long been one of my favourite tools, and it just keeps getting better.
"I'm still exploring myself." - TWSS
Sorry, couldn't resist. :)
You could use an ajax prefilter to make the normalization process even more transparent.
Prefilters are called before any callback is added to the ajax promise (even those provided in the settings object). So you can easily listen to the ajax promise, apply your normalization and then replace the jqXHR promise methods with those of your internal deferred (so that further callbacks get added to the normalizing deferred).
It uses an as-of-yet undocumented feature of promise(): it can accept an object as a parameter and, in such a situation, will add promise methods corresponding to the underlying deferred onto the given object (pure aspect-oriented programming here).
Since success & error are just aliases of done & fail respectively and are not part of the promise interface, you still have to manually update them.
Nice article on deferred
More deferred functions at http://ram-interest.blogspot.com/2011/02/jquery-15-deferred-function.html">Deferred
Ha ha - always appreciated ;)
First off, awesome work on the Deferred and the AJAX rewrite - very cool stuff. As far as the code, though, are you saying that the $.ajaxPrefilter() is not defined? Or the use of objects as an argument to promise? I don't see the former documented anywhere either.
I was looking at the documentation for a filter, but I could only find one for the outgoing request, not the incoming response. Or rather, not an incoming response before the success/error branching. Meaning, I found a way to clean the data, but only for success responses (20x status codes).
... actually, on Google, I just found the preFilter stuff. Looks like they just not hooked up to the navigation yet.
That said, looks like an awesome approach! Thanks for the dynamite tip.
Also, enjoyed your interview on YayQuery. Everyone loves saying your name :)
Thanks again for your guidance - I tried to take what you said and break it down:
I found the whole deferred.promise( obj ) thing very thought provoking!
Yeah, this kind of trickery is exactly why closures are used to create the deferred objects rather than a more traditional prototype-based approach. It's actually used internally in ajax itself to attach the promise methods onto the jqXHR object.
Glad I could help and thanks for all these awesome posts, you definitely explain things much much better than I would ever hope to.
I don't know how you guys keep the jQuery source code modeled in your heads! Writing the last post, I spend like 45 minutes just jumping back and forth between parts of the source trying to figure out what was going on.
The stuff you guys do simply blows my mind!
Oh well, it's kinda imprinted into your brain after months deep down into code, refactoring, pondering design decisions, going back and forth because you're never sure what the best solution is, or if there actually is a "best" solution to begin with (I always feel like all I do is compromise all the time actually).
I wouldn't call that a fit and I'm pretty sure it would be of interest to some psychology studies regarding obsessive behaviour (did I mention I sometimes wake up in the middle of the night because I *have* to refactor a piece of code?) ;)
Beside, there are parts of jQuery I think I haven't read yet :P
What I find rewarding is seeing people using the lib and making all kind of awesome, useful stuff with it. I would never have thought of your use-case before reading about it on your blog and I'm quite relieved prefilters and deferreds combined can offer a relatively "clean", if a bit complex, solution.
With the posts you've written on ajax and deferreds, you help developpers use jQuery in better ways and you also help us, in the project, get a sense of reality and find opportunities to better jQuery itself. And that, my friend, blows *my* mind.
Good sir, you are too kind :) At the end of the day, it's all just one big awesome community of like-minded people who want to do awesome stuff with jQuery :D
Great article. You might want to take a look at pipe(), I find it very helpful for normalizing responses.
Ben, I can't thank you enough for your informative posts on jQuery's deferred object. It's really wonderful and has saved me from callback hell!
Question for you: I would like to do something analogous to the following in your example, but am not sure how best to go about it...
Check a parameter for appropriate values in the saveContact() method before actually making the ajax call. Then failing the promise if the parameter value was wrong so the saveAction.fail() is fired. Imagine the ajax call returns a poor error message, I'd like to do a simple check and save myself from making the call at all and returning a more useful message.
More generally speaking, can we use the deferred object to add error handling/checking to an API function that returns a promise so that the .fail is fired?
I believe I have the answer thanks to Eric Hynd's "chaining hotness". Create your own deferred in the API wrapper and you can reject/resolve in your validation or error handling as needed.
I didn't realize that the deferred object works that way. At first it seems like it always needs to wait for an asynchronous call to come back, but so long as your function returns a promise you can resolve or reject whenever you want.