Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at the NYC Tech Talk Meetup (Aug. 2010) with:

Using Deferred Objects In jQuery 1.5 To Normalize API Responses

By Ben Nadel on

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.

api.cfm

  • <!---
  • Create the default response object. This is what will be
  • serialized and return the client in JSON format.
  •  
  • NOTE: We are using array-notation here in order to maintain case
  • for easier use within the client-side environment (Javascript).
  • --->
  • <cfset response = {} />
  • <cfset response[ "statusCode" ] = "200" />
  • <cfset response[ "statusText" ] = "OK" />
  • <cfset response[ "success" ] = true />
  • <cfset response[ "data" ] = "" />
  • <cfset response[ "errors" ] = [] />
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!--- Randomly cause unexpected errors. --->
  • <cfif (randRange( 1, 3 ) eq 2)>
  •  
  • <cfthrow type="eyeIzInYourServerMaykinItBetterz" />
  •  
  • </cfif>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • Wrap the entire processing of the form in a try/catch so that
  • we can return the appropriate response code should something
  • go wrong.
  • --->
  • <cftry>
  •  
  • <!--- Param the incoming form values. --->
  • <cfparam name="form.username" type="string" default="" />
  • <cfparam name="form.name" type="string" default="" />
  • <cfparam name="form.age" type="string" default="0" />
  •  
  •  
  • <!--- Make sure the user is validated. --->
  • <cfif (form.username neq "ben")>
  •  
  • <!--- This user is not authorized to access this API. --->
  • <cfthrow
  • type="NotAuthorized"
  • />
  •  
  • </cfif>
  •  
  •  
  • <!--- Validate the data. --->
  • <cfif !len( form.name )>
  •  
  • <!--- Append an error. --->
  • <cfset arrayAppend(
  • response.errors,
  • "Please enter a valid name."
  • ) />
  •  
  • </cfif>
  •  
  • <!--- Validate the data. --->
  • <cfif !isNumeric( form.age )>
  •  
  • <!--- Append an error. --->
  • <cfset arrayAppend(
  • response.errors,
  • "Please enter a numeric age."
  • ) />
  •  
  • <cfelseif (form.age lt 18)>
  •  
  • <!--- Append an error. --->
  • <cfset arrayAppend(
  • response.errors,
  • "Please enter an age greater than 18."
  • ) />
  •  
  • </cfif>
  •  
  • <!--- Check to see if there are any errors. --->
  • <cfif arrayLen( response.errors )>
  •  
  • <!--- Throw a bad request error. --->
  • <cfthrow
  • type="BadRequest"
  • message="Invalid Request Parameters"
  • />
  •  
  • </cfif>
  •  
  •  
  •  
  • <!---
  • If we have made it this far then the data is valid and
  • can be persisted to the database. Of course, this is just
  • an exploration of jQuery, so we don't need a database...
  • just return a valid data point.
  • --->
  • <cfset response.data = "#form.name# (#form.age#)" />
  •  
  •  
  •  
  • <!--- Not authorized.. --->
  • <cfcatch type="NotAuthorized">
  •  
  • <!--- Set authorization message. --->
  • <cfset arrayAppend(
  • response.errors,
  • "You are not authorized to use this API."
  • ) />
  •  
  • <!--- Flag the security concern. --->
  • <cfset response.statusCode = "401" />
  • <cfset response.statusText = "Unauthorized" />
  • <cfset response.success = false />
  •  
  • </cfcatch>
  •  
  • <!--- Bad request errors (data validation). --->
  • <cfcatch type="BadRequest">
  •  
  • <!--- Flag the server error. --->
  • <cfset response.statusCode = "400" />
  • <cfset response.statusText = "Bad Request" />
  • <cfset response.success = false />
  •  
  • </cfcatch>
  •  
  • <!--- Unexpected error. --->
  • <cfcatch>
  •  
  • <!--- An unknown error occurred. --->
  • <cfset arrayAppend(
  • response.errors,
  • "An unexpected error occurred."
  • ) />
  •  
  • <!--- Flag the server error. --->
  • <cfset response.statusCode = "500" />
  • <cfset response.statusText = "Internal Server Error" />
  • <cfset response.success = false />
  •  
  • </cfcatch>
  •  
  • </cftry>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • Set the appropriate status code for the response.
  •  
  • NOTE: Only 20x status codes will result in a success handler
  • (or a "done" deferred handler); anything else will result in an
  • error handler (or a "fail" deferred handler) being called.
  • --->
  • <cfheader
  • statuscode="#response.statusCode#"
  • statustext="#response.statusText#"
  • />
  •  
  • <!--- Return the JSON response to the client. --->
  • <cfcontent
  • type="application/json"
  • variable="#toBinary( toBase64( serializeJSON( response ) ) )#"
  • />

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.

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Playing With Deferred And AJAX In jQuery 1.5</title>
  • <script type="text/javascript" src="../jquery-1.5.js"></script>
  • </head>
  • <body>
  •  
  • <h1>
  • Playing With Deferred And AJAX In jQuery 1.5
  • </h1>
  •  
  • <form>
  •  
  • <h2>
  • Enter Data
  • </h2>
  •  
  • <p class="message" style="display: none ;">
  • <!--
  • This is where the confirmation message will go
  • on the AJAX request completion.
  • --->
  • </p>
  •  
  • <p>
  • Username:
  • <input type="text" name="username" size="20" />
  • </p>
  •  
  • <p>
  • Name:
  • <input type="text" name="name" size="20" />
  • </p>
  •  
  • <p>
  • Age:
  • <input type="text" name="age" size="5" />
  • </p>
  •  
  • <p>
  • <input type="submit" value="Save Contact" />
  • </p>
  •  
  • </form>
  •  
  •  
  • <!-- --------------------------------------------------- -->
  • <!-- --------------------------------------------------- -->
  •  
  •  
  • <script type="text/javascript">
  •  
  • // Store DOM references.
  • var form = $( "form" );
  • var message = $( "p.message" );
  • var username = form.find( "input[ name = 'username' ]" );
  • var contactName = form.find( "input[ name = 'name' ]" );
  • var contactAge = form.find( "input[ name = 'age' ]" );
  •  
  •  
  • // Bind to the form submission error to handle it via AJAX
  • // rather than through the standard HTTP request.
  • form.submit(
  • function( event ){
  •  
  • // Prevent the default browser behavior.
  • event.preventDefault();
  •  
  • // Try to save the contact to the server. The
  • // saveContact() method returnes a promise object
  • // which will come back with a result eventually.
  • // Depending on how it resolves, either the done()
  • // or fail() event handlers will be invoked.
  • //
  • // NOTE: This return object can be chained; but for
  • // clarity reasons, I am leaving these as one-offs.
  • var saveAction = saveContact(
  • username.val(),
  • contactName.val(),
  • contactAge.val()
  • );
  •  
  • // Hook into the "success" outcome.
  • saveAction.done(
  • function( response ){
  •  
  • // Output success message.
  • message.text(
  • "Contact " + response.data + " saved!"
  • );
  •  
  • // Show the message.
  • message.show();
  •  
  • }
  • );
  •  
  • // Hook into the "fail" outcome.
  • saveAction.fail(
  • function( response ){
  •  
  • // Output fail message.
  • message.html(
  • "Please review the following<br />-- " +
  • response.errors.join( "<br />-- " )
  • );
  •  
  • // Show the message.
  • message.show();
  •  
  • }
  • );
  •  
  • }
  • );
  •  
  •  
  • // I save the contact data.
  • function saveContact( username, name, age ){
  • // Initiate the AJAX request. This will return an
  • // AJAX promise object that maps (mostly) to the
  • // standard done/fail promise interface.
  • var request = $.ajax({
  • type: "post",
  • url: "./api.cfm",
  • data: {
  • username: username,
  • name: name,
  • age: age
  • }
  • });
  •  
  • // Return a normalized request promise.
  • return(
  • normalizeAJAXResponse( request )
  • );
  • }
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I take the AJAX request and return a new deferred object
  • // that is able to normalize the response from the server so
  • // that all of the done/fail handlers can treat the incoming
  • // data in a standardized, unifor manner.
  • function normalizeAJAXResponse( request ){
  • // Create an object to hold our normalized deferred.
  • // Since AJAX errors don't get parsed, we need to
  • // create a proxy that will handle that for us.
  • var normalizedRequest = $.Deferred();
  •  
  • // Bind the done/fail aspects of the original AJAX
  • // request. We can use these hooks to resolve our
  • // normalized AJAX request.
  • request.then(
  •  
  • // SUCCESS hook. ------ //
  • // Simply pass this onto the normalized
  • // response object (with a success-based resolve).
  • normalizedRequest.resolve,
  •  
  • // FAIL hook. -------- //
  • function( xhr ){
  •  
  • // Check to see what the status code of the
  • // response was. A 500 response will represent
  • // an unexpected error. Anything else is simply
  • // a non-20x error that needs to be manually
  • // parsed.
  • if (xhr.status == 500){
  •  
  • // Normalize the fail() response.
  • normalizedRequest.reject(
  • {
  • success: false,
  • data: "",
  • errors: [ "Unexpected error." ],
  • statusCode: xhr.statusCode()
  • },
  • "error",
  • xhr
  • );
  •  
  • } else {
  •  
  • // Normalize the non-500 "failures."
  • normalizedRequest.reject(
  • $.parseJSON( xhr.responseText ),
  • "success",
  • xhr
  • );
  •  
  • }
  •  
  • }
  •  
  • );
  •  
  • // Return the normalized request object. This deferred
  • // object can then be used by the calling context to
  • // deal with success and failure. By normalizing it, both
  • // the success and error handlers will be able to assume
  • // that the response is coming in the same format.
  • //
  • // NOTE: Calling the .promise() method creates a read-only
  • // interace to the deferred object such that the receiving
  • // context can only hook into the object, not mutate it.
  • return( normalizedRequest.promise() );
  • }
  •  
  • </script>
  •  
  • </body>
  • </html>

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.




Reader Comments

Excellent blog. Best part being, covers all the new features of $.Deferred(). Bookmarked as a reference article. Thanks.

Reply to this Comment

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. :)

Reply to this Comment

Hi Ben,

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).

http://jsfiddle.net/a289y/

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.

Reply to this Comment

@Kerr,

Ha ha - always appreciated ;)

@Julian,

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 :)

Reply to this Comment

@Ben,

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.

Reply to this Comment

@Julian,

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!

Reply to this Comment

@Ben,

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.

Reply to this Comment

@Julian,

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

Reply to this Comment

Great article. You might want to take a look at pipe(), I find it very helpful for normalizing responses.

Reply to this Comment

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?

Thanks!

Reply to this Comment

@Adam,

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.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.