From Callback Hell

to the

JavaScript “Promise” Land

About Me

Brandon Martinez | brandonmartinez.com | @brandonmartinez

Skyline Technologies (We're Hiring!) | skylinetechnologies.com | @skylinetweets

What's a Callback?

In JavaScript, functions are considered first-class citizens; they are able to be passed as arguments or stored in variables or fields.

Callbacks are functions that are passed as an argument to another function.


function runThisFunction(myCallbackFunction) {
    myCallbackFunction();
}

runThisFunction(function() {
	console.log('Kind of useless, but I see your point.');
});
                        

Why Use a Callback?

  1. You want to allow the calling code to encapsulate some logic that will be performed within the called function (e.g. an onclick handler).
  2. You want logic to be performed after an asynchronous operation (e.g. an AJAX request succeeds or fails).

Here is a common example, passing an anonymous callback function to be used after the success of a jQuery AJAX request.


$.ajax({
    url: '/myapi/',
    type: 'POST',
    data: { Id: 123, Description: 'Call me back, bro!' },
    success: function(data) {
        // Our callback function; this will be executed
        // after the async query is finished
        console.log(data);
        myData.push(data);
    }
});
    						

That doesn't seem bad, does it? But what happens when things get more complex?

Welcome to Callback Hell

First Stop, the Pyramid of Doom

Have you ever written code that looks like this?


$.ajax({
    url: '/myapi/',
    type: 'POST',
    data: { Id: 123, Description: 'Call me back, bro!' },
    success: function(returnedData) {
        // Success! Time to call our next service
        $.ajax({
            url: '/mynextapi/',
            type: 'POST',
            data: returnedData,
            success: function(data) {
                $('#messages').animate({
                    opacity: 0.25,
                    left: "+=50",
                    height: "toggle"
                }, 5000, function() {
                    setTimeout(function() {
                        $(this).val('Finished!');
                        // How did we get here???
                    }, 1000);
                });
            }
        });
    }
});
						

The Pyramid of Doom

Deep, intermingled heirachies of functions, often called asynchronously.

Scope is very difficult to track.

Code maintainability SUCKS!

Can't We Refactor?

Can't We Refactor?

Of course! Let's pull all of those anonymous functions into named functions. That will help, right?



function animateAfterFinalPosting(){
    $('#messages').animate({
        opacity: 0.25,
        left: "+=50",
        height: "toggle"
    }, 5000, function() {
        setTimeout(function() {
            $(this).val('Finished!');
        }, 1000);
    });
}

function postInitialDataSuccess(returnedData) {
    // Success! Time to call our next service
    $.ajax({
        url: '/mynextapi/',
        type: 'POST',
        data: returnedData,
        success: animateAfterFinalPosting
    });
}

function postInitialData(myData) {
    $.ajax({
        url: '/myapi/',
        type: 'POST',
        data: myData,
        success: postInitialDataSuccess
    });	
}

postInitialData({
    Id: 123,
    Description: "Let's dive in!"
});
						

Better, right?

Well…

Can't We Refactor?

Code has become more manageable, however:

  • Functions are now coupled with their immediate child(ren).
  • Adding or removing pieces of the process can cause a ripple effect.
  • Following the business logic and process can become difficult (choose your own adventure, anyone?).

So what can we do?

To the "Promise" Land!

What's a Promise?

First, a discalaimer: Promises, or specifically the Promises/A+ standardization, are currently available in JavaScript's currently standardizing ECMAScript 6 additions. As such, they are not available in all browsers without using an additional library, like Q.js, or a polyfill.

For more information, I highly recommend reading the JavaScript Promises article at HTML5 Rocks (http://bmtn.us/1ikMPTc).

What's a Promise?

A promise is an abstraction over asynchronous functions and tasks in JavaScript.

At the minimum, a promise is a JavaScript object with a then function.

The then function returns a fullfilled value or throws an exception. The former allows you to chain multiple promises together (fluent-style).

Promises can have four states: fulfilled, rejected, pending, or settled.

A small sample of a promise chain. We're using anonymous functions here, but notice how much more readable the code is. Not only that, but it's linear!


// Using Q, basic concepts apply to Promises/A+
Q({
    Id: 123,
    Description: "Let's dive in!"
}).then(function(data) {
    // jQuery isn't standard-compliant, wrap in a proper promise and return
    return Q($.ajax({
        url: '/myapi/',
        type: 'POST',
        data: data
    }));
}).then(function(returnedData) {
    return Q($.ajax({
        url: '/mynextapi/',
        type: 'POST',
        data: returnedData
    }));
}).then(function(nextReturnedData) {
        $('#messages').animate({
        opacity: 0.25,
        left: "+=50",
        height: "toggle"
    }, 5000, function() {
        setTimeout(function() {
            $(this).val('Finished!');
        }, 1000);
    });
}).catch(function(error) {
    // Easy error handling!
    console.log(error);
});
						

Now let's move our anonymous functions into their named counterparts.


function sendFirstDataPost(data) {
    // jQuery isn't standard-compliant, wrap in a proper promise and return
    return Q($.ajax({
        url: '/myapi/',
        type: 'POST',
        data: data
    }));
}

function sendSecondDataPost(returnedData) {
    return Q($.ajax({
        url: '/mynextapi/',
        type: 'POST',
        data: returnedData
    }));
}

// note that we don't _have_ to provide the
// previously returned data; javascript lets
// us ignore arguments in callbacks
function animateMessageBox() {
	$('#messages').animate({
        opacity: 0.25,
        left: "+=50",
        height: "toggle"
    }, 5000, function() {
        setTimeout(function() {
            $(this).val('Finished!');
        }, 1000);
    });
}

function errorHandler(error) {
    // Easy error handling! This method could
    // potentially be reused in any promise chain
    console.log(error);
}

// Using Q, basic concepts apply to Promises/A+
Q({
        Id: 123,
        Description: "Let's dive in!"
    })
    .then(sendFirstDataPost)
    .then(sendSecondDataPost)
    .then(animateMessageBox)
    .catch(errorHandler);
						

That Easy?

Yes!

How Do Promises
Improve My Code?

  • "Flattens" the chain
  • Promotes linear, workflow-oriented business logic
  • Promises can return promises, which can each be composed of their own chains (example soon).
  • "Thenable" functions are mostly atomic, taking data in and sending data out (greatly reducing scope issues).
  • Flexibility!

Demos

We've been tasked with creating a lightweight web interface for retrieving and processing CommandSets from a web API:

Core Specifications

  • On page load, retrieve a list of CommandSets from a web API, displaying them on the page.
  • The user is allowed to refresh the current data to retrieve the next batch.
  • The data can be cleared.
  • Processing the data send the commands individually to a web API, returning a success or failure result (displaying any additional information if given).

Demo 1

Using anonymous callbacks, we'll send all commands to the web API and wait for a response.

Launch the Demo

Demo 1

Recap

  • The "quick and dirty" approach to processing.
  • No (easy) way to track when all requests are complete.
  • Possibility of scope issues (see inline closure in source).

Demo 2

Taking the previous example and breaking it into a promise-style chain, we send all commands to a web API and wait for a response.

Demo 2

Additional Specifications

In addition to the core specifications:

  • The user cannot initiate another batch process until all commands have returned.

Launch the Demo

Demo 2

Recap

  • Each command set is easily flattened into multiple promise chains for each command.
  • Returning the command promise chains to the main chain allows us to track when all child chains complete.
  • Each piece of our workflow is linear! This-then-this-then-this-etc.

Demo 3

Using named callbacks to help clean-up the code from the first demo and add additional functionality.

Demo 3

Additional Specifications

In addition to the previous specifications:

  • The command sets must have their commands processed in order, waiting for the previous to be completed. If the previous failed, stop that command set from continuing.
  • The user can stop processing at any time, blocking any new commands from being sent.

Launch the Demo

Demo 3

Recap

  • While cleaner than purely anonymous functions, it can still be difficult to see where the flow of the application goes in code.
  • In order to get "synchronous" AJAX, it requires passing current progress through the entire chain.
  • Tracking when the entire process finishes requires a counter and a comparison at the end of every child chain.

Demo 4

Using the previous demo as a base, we've rewritten the logic to produce promise chains. Additionally, we show how easy it is to control the flow of child chains, as well as knowing when the entire process is finished.

Launch the Demo

Demo 4

Recap

  • While a more complicated example, it shows how powerful chaining promises can be, especially when compared to injecting callbacks.
  • The ability to dynamically create child sequences, and then to wait for all fo them to be fulfilled, is invaluable when compared to the callback-based approach.

How Can I Start Making Promises?

While browsers continue to add and support the full Promises/A+ standard, you can still enjoy them in most ECMAScript 5 browers.

  • Q
  • RSVP.js
  • when

But I want to use the standard!

The JavaScript Promises article at HTML5 Rocks (http://bmtn.us/1ikMPTc) has a polyfill that can be used to support older browsers, as well (based on RSVP.js).

But I want to use the standard!

To see if your favorite browser supports the Promise standard, visit caniuse.com/promises.

Demo 5

The same as Demo 4, just using native Promises (no polyfill, so check your browser support!).

Launch the Demo

Demo 5

Recap and Diff

Questions?

The End.

Promise

Brandon Martinez | brandonmartinez.com | @brandonmartinez

Skyline Technologies (We're Hiring!) | skylinetechnologies.com | @skylinetweets