Wednesday, September 12, 2012

I $.Promise - Part 1 - Why do jQuery Promises exist?

When I first learned about Promises, I couldn't rationalize their existence.  I simply didn't know what they were for.  Sure, I had been told how great they were, but I made due with callbacks and events and never saw a need for anything else.  After having been exposed to this brand new world, I don't know how I managed without them!

In this first post, as an introduction to jQuery Promises, I hope to answer the question as to why Promises exist and solve some very very basic problems traditionally solved by callback functions.

Building up to promises from traditional callbacks

So you've got this thing called a Promise.  Now what the heck is it?  Well, let's start by thinking about the success callback function of $.get().

$.get('...', function () {
    console.log("Data gotten!");
});

Okay, seems well and good.  But now what if you want to execute more than one success function? Maybe the success function is a function that calls two more success functions!  That'll be cool!

var firstSuccessFunction = function () { 
    console.log('Update thing A'); 
};
var secondSuccessFunction = function () { 
    console.log('Update thing B'); 
};

var executeSuccessFunctions = function () {
    firstSuccessFunction();
    secondSuccessFunction();
};

$.get('...', executeSuccessFunctions);

One major problem with this: The authority on what gets executed upon $.get() success is the executeSuccessFunctions function and not whomever actually called $.get()!

In order to take control away from executeSuccessFunctions (because it might not know what we want to do or exist in a different scope all together), we need to give an agnostic execute function to $.get() and let the caller construct what he wants to execute.

var successFunctions = [];
var firstSuccessFunction = function () { 
    console.log('Update thing A'); 
};
var secondSuccessFunction = function () { 
    console.log('Update thing B'); 
};

successFunctions.push(firstSuccessFunction);
successFunctions.push(secondSuccessFunction);

var executeSuccessFunctions = function () {
    for (var i = 0; i < successFunctions.length; i++) {
        successFunctions[i](); // Execute it
    }
}

$.get('...', executeSuccessFunctions);

More unsolved problems

  • Initial appearances might be that you can push a function onto successFunctions after the $.get() line and have it execute on success.  What this would really do is create a race condition: if the $.get() returns after you do this, no problem; however, if it returns before you push a new function onto the successFunctions stack, it'll never get executed.  Net result: we still need to define all of our success callbacks before we actually make our AJAX call.
  • We've written unnecessary plumbing code that'd need to get pasted all over the place for executing each function in an array or whatever other logic might exist.
  • We cannot easily accomodate all possible code paths.  What if the AJAX call itself fails?  Do I then need to construct a new errorFunctions array for storing those?  What if there's overlap and I want to execute firstSuccessFunction under both scenarios?  The pure plumbing required to hook all this up is ginormous.  

Well, Promises solve all of these problems and more via an entirely different convention.

Rather than giving an array of success callbacks to $.ajax() to execute when it's done doing its thing, it delegates that responsibility to a third party object and returns this object immediately to the caller.

Promise-based implementation

Here's our example again using Promises.

// Don't need to give a callback to $.get() as it 
// isn't going to execute our callbacks, on
// AJAX success it's going to tell the Promise
// it returned to execute **its** callbacks
// instead. This call to resolve() by $.ajax() 
// happens when the server responds.

var getting = $.get('...');

var firstSuccessFunction = function () { 
    console.log('Update thing A'); 
};
var secondSuccessFunction = function () { 
    console.log('Update thing B'); 
};

// analogous to successFunctions.push from before
getting.done(firstSuccessFunction); 
getting.done(secondSuccessFunction);


With that tiny bit of code, we've solved all of our stated problems with standard callbacks:
  • The caller of $.get() is the authority of what gets executed upon the success or failure of $.get().
  • We can define and attach a callback function at any time--before or after $.ajax() has returned and is resolved, even in an entirely new scope simply by handing off the Promise object.  Whether the Promise is resolved or not yet is irrelevant: if it is, the function passed to done() will execute immediately.  If it isn't, it'll execute later when the Promise resolved.
  • There's no plumbing code about how to execute things or what callback chain to execute.  That's handled by the Promise itself and the authority over the Promise (in our case $.get) respectively.
  • To create an entirely different chain of callbacks on failure, we just attach new handlers via .fail() instead.  If we want something to execute no matter what, that's when we use .always().

The code shown here is deliberately simple, but I hope when you look at it you do not merely see callback equivalents with a different syntax.  Promises are a whole new paradigm that will significantly change the way you structure your applications.

In short, Promises are awesome.

No comments: