jQuery – making .then a little more useful

Published on: 09:04, 02-04-2011 Filed Under: Uncategorized View Comments

Edit 04.06.2011: This feature is now a part of jQuery 1.6 (commit bb99899c) and called .always. Checkout .chain too! :)

The introduction of Deferreds into jQuery in 1.5 is quite a handy feature. Particularly, being able to make an ajax request and assign callbacks onto the returned jqXHR.

However, at least for me, .then doesn’t work the way my brain thinks it should (Thanks to Ben Alman bringing this up). When you get a promise back from the deferred, it has three methods: done, fail, and then

done and fail do exactly what you’d think: attach functions to be called if the deferred is resolved or rejected.

then is currently a shortcut method for the two.

$.ajax({
    url: 'someUrl'
}).then( doneFn, failFn );

The word then, for me, is much more conclusive. It isn’t a shortcut, it’s an ultimatum. Thus, I expect .then to work like:

$.ajax({
    url: 'someUrl'
}).then( willGetCalledNoMatterWhat );

So, if you would rather trade in your shortcut for power, be explicit and have control, or just be part of the cool club – here’s how to make .then work the way you want here’s how to add the functionality yourself. I’ve been persuaded by Rebecca and Colin to not alter the functionality of an existing, documented method, but rather, add the additional functionally. Their point was simple: by changing the functionality of how somethin existing and documented operates to work contrary the way it should, anyone stepping into code that had modified then would have a hell of a time figuring out why it wasn’t working to spec.

// Wrap in an IIFE and preserve $ incase of $.noConflict();
(function( $ ) {

  // Cache a copy of $.Deferred as it is
  // right now so it can be called later
  var _Deferred = $.Deferred;

  // Overwrite $.Deferred with a new function
  // that takes the same argument
  $.Deferred = function( func ) {

    // Call the original $.Deferred method (the
    // cached copy ) using .apply to assign
    // the same context and send the 'func'
    // argument
    var d = _Deferred.apply( this, func ),
         // copy the created .promise method
         // to duck-punch further down
          _promise = d.promise;

    // add our new 'then' type function, 'always'
    d.always = function( fn ) {
        return d.then( fn, fn );
    };
    // duck-punch the created promise method
    // to use the newly created .always method
    d.promise = function( obj ) {
        var promise = _promise.call( this, obj );
        promise.always = d.always;
        return promise;
    };  

    // return the our modified deferred from
    // _Deferred, but with our new .always
    return d;
  };
})( jQuery );

The Result

$.ajax({
    url: 'someUrl'
}).always( function() {
   /* i get called no matter WHAT */
});

How it works

This duck-punches the existing $.Deferred function with a wrapper function. $.Deferred creates a new $._Deferred and makes the .then, .done, and .fail methods on the fly. By saving a copy of the old function, we can let it go about its usual business and add the .always function to both the deferred and its promise method.

Demo

Demo at jsfiddle.net

Smaller Version

(function($){var o=$.Deferred;$.Deferred=function(a){var d=o.apply(this,a),p=d.promise;d.always=function(f){return d.then(f,f);};d.promise=function(j){var n=p.call(this,j);n.always=d.always;return n;};return d;}})(jQuery);
  • Anonymous

    Hated this about $.then, thanks!

  • http://benalman.com/ “Cowboy” Ben Alman

    Dan, you need to return this; inside your .then function.

  • Anonymous

    oy, forgot about chaining the promise – thanks :D ( and included )

  • http://benalman.com/ “Cowboy” Ben Alman

    Great post, Dan. I’ve also got a little plugin that does something different, but related:

    jQuery whenthen: jQuery’s “when” and “then” all rolled up together.
    https://gist.github.com/867252

  • http://twitter.com/rmurphey Rebecca Murphey

    i seem to have double-posted. sorry about that.

  • http://twitter.com/rmurphey Rebecca Murphey

    If you’re inclined to make this change, just please remember that it controverts the spec proposal for Promises/A (http://wiki.commonjs.org/wiki/Promises/A ). While I understand that .then() may not behave as you’d like, changing user expectations is fraught with its own issues. I’d go for a new method name rather than changing the meaning of one that’s being embraced across promise implementations.

  • Colin S

    By fucking with then, you have just broken compatibility with the Promises/A spec. You are also returning the wrong thing; if you want to return the promise then you need to call deferredObj.promise().

  • Anonymous

    I see what you mean, and thanks for pointing that out. I’ve followed your suggesting and renamed it .thenDo to facilitate the functionally I’d like to see without messing with .then.

  • Anonymous

    I put yours and Rebecca’s feedback into the post – it was a naming error, I wanted to return the deferred; thanks for the note :)

  • http://benalman.com/ “Cowboy” Ben Alman

    How about calling it .always( fn ) and landing it in jQuery? Would that conflict with any spec proposals?

  • Anonymous

    I think if we could get some agreement on a name it would make a great addition – always, thenDo, ultimately, eventually, please, callThisWhenYoureDone – something :p

  • http://pulse.yahoo.com/_EZRH2WK7QQMBNRTDYKAMKZVKBQ G E E K C U B E

    enjoy your posts :)
    but isn’t .then( willGetCalledNoMatterWhat ); kind of the same as .always( function() {   /* i get called no matter WHAT *
    ???not picking (because i’m not mentally armed enough for a debate :-P ), but it seems like you’re just merely adapting another use of .then  (jmho)i ask this because i want to know where this convo left off (4 months ago) – i’d like to implement it if it’s stable and a good practice with the changes Rebecca mentions below(?)Thanks, geekbuntu

  • Anonymous

    I made an edit to the top of the article when .always was added to jQuery core – this article was prior to that and was more of a proof of concept on the idea.

    .then( fn1 ) will only run on success. .then( fn1, fn1 ) will run on both success and failure, which is the same as .always( fn1 ). 

    Hope this clears it up for ya

blog comments powered by Disqus