Promises, Promises

An introduction to Deferred objects in Javascript

http://slides.com/taxilian/promises

Richard Bateman
Assisted by:







Rob Porter

Ben Loveridge

Michael Stufflebeam

Jarom Loveridge

Who am I?


  • Lead Architect at GradeCam

Presentation goals

What do we want to accomplish during our time here?

  • Slow presentations bore me -- we're going to be moving right along.
    • If you have a question, speak up ! If you say nothing and don't learn what you wanted to, it's your fault and not mine.
  • We want to learn:
    • What are Promises?
    • What do they give us?
    • How do we use them?
    • What libraries are available, and what is the future?
  • At the end of the presentation, you should be able to answer the question:
    • What can I do with a Promise that I can't do with a callback?

At the end of the presentation, you should also be able to use Promises!

Javascript is asynchronous

  • Client-side javascript is often asynchronous
    • Ever-ubiquitous AJAX / JSONP
    • Web sockets
    • New HTML5 APIs, such as FileReader
    • Web workers
    • User interactions (e.g. modal dialog results)

  • Server-side javascript (node.js) is nearly always asynchronous
    • Disk access
    • Network access
    • Database access
    • etc

Asynchronous code is ugly

function getUserInfo(id, callback) {
    $.ajax({
        url: "/user/" + id,
        type: 'GET',
        dataType: 'html',
        success: function(data, textStatus, xhr) {
            callback(null, data);
        },
        error: function(xhr, textStatus, errorThrown) {
            callback(new Error(textStatus), errorThrown);
        }
    });
}
function getUserComments(id, callback) {
    $.ajax({
        url: "/user/" + id + "/comments",
        type: 'GET',
        dataType: 'html',
        success: function(data, textStatus, xhr) {
            callback(null, data);
        },
        error: function(xhr, textStatus, errorThrown) {
            callback(new Error(textStatus), errorThrown);
        }
    });
}
Here are a couple of standard AJAX calling functions.

Nested functions are verbose

// Get the user info
getUserInfo(1, function(err, userDoc) {
    if (!err) {
        // get the user comments
        getUserComments(1, function(err, commentArray) {
            if (!err) {
                DisplayUserInfoWithComments(userDoc, commentArray);
            } else {
                // Do something to handle errors
            }
        });
    } else {
        // Do something to handle errors
    }
}

There are a few issues with this code; for one thing, in this case we don't actually need to wait for one to complete before requesting the other.  We can request them both.

Let's fix that.

Parallel calls are even more verbose

// Get the user info
var userDoc = null;
var commentArray = null;
var wasError = false
getUserInfo(1, function(err, inDoc) {
    if (!err) {
        userDoc = inDoc;
        if (commentArray) {
            DisplayUserInfoWithComments(userDoc, commentArray);
        }
    } else {
        wasError = err;
        wasError.errThrown = arguments[1];
    }
});
getUserComments(1, function(err, inArray) {
    if (!err) {
        commentArray = inArray;
        if (userDoc) {
            DisplayUserInfoWithComments(userDoc, commentArray);
        }
    } else {
        wasError = err;
        wasError.errThrown = arguments[1];
    }
});
Wow, two requests asynchronously!  And my, isn't that code beautiful...

Let's try it using jQuery Deferred

// Deferred method
function getUserInfo(id) {
    return $.ajax({
        url: "/user/" + id,
        type: 'GET',
        dataType: 'html',
    }).then(function(data, textStatus, xhr) { return data; });
}
function getUserComments(id, callback) {
    $.ajax({
        url: "/user/" + id + "/comments",
        type: 'GET',
        dataType: 'html'
    }).then(function(data, textStatus, xhr) { return data; });
}
  • Shorter methods
  • Where did the error handling go?
  • Why are we doing a no-op then function?
    • jQuery normally resolves ajax to 3 parameters; we only care about the first.

Using the ajax calls with Deferred

// Get the user info; deferred method
var userDfd = getUserInfo(1);
var commentsDfd = getUserComments(1);
DisplayUserInfoWithComments(userDfd, commentsDfd);

What, skeptical?



Okay, it does need one more change:
function DisplayUserInfoWithComments(userDfd, commentsDfd) {
        $.when(userDfd, commentsDfd).then(function(user, comments) {
            // Previous contents of DisplayUserInfoWithComments in here
        }, function(err) {
            // Handle the error
        });
    }
    

A promise is an asynchronous return value


With traditional callbacks you are saying "run, and when you're finished call this function".

With promises you run the function and receive a result that "promises" to resolve to what you want. ... eventually.

The advantage to this is that you can then pass around the "promise" and the control doesn't leave the scope of the function until you are ready to actually use the value that it will resolve to.

Deal with it when you're ready

There are two types of functionality we are dealing with:

  • Operations that do something asynchronously and return a result
  • Operations that take data and do something with it





Which of these should care about program flow?

Separation of concerns


Some code is just "glue" -- it calls other functions and then passes values where they need to go.


Some code is deeply involved with the data.


Some code knows what should happen when there is an error, and some code does not.


Promises give us an easy way to put the responsibility in the correct part of the code, but still pass the data through the other parts.

Case study


Express REST API made easy with promises




https://github.com/taxilian-promises/express-demo

Waiting for multiple Async operations

from bin/make_fake_data

function cleanDatabase() {
    var waitFor = [];

    console.log("Cleaning database");
    waitFor.push(dropCollection('users'));
    waitFor.push(dropCollection('comments'));

    return Q.all(waitFor);
}
Most Promise libraries have something like Q.all -- jQuery has $.when, other libraries call it something different.  It accepts multiple Promises and resolves once all input Promises are resolve to an array containing the resolved value of each promise.

If any of those Promises reject then the Promise returned by Q.all will immediately reject to the error that Promise rejected with.

Sequential Asynchronous operations

from bin/make_fake_data

var doneDfd = Q(null); // Initial resolved promise

doneDfd = doneDfd.then(cleanDatabase());

// create users
var userDfd, commentsDfd;
for (var i = 0; i < 20; ++i) {
    // Note that since this is sequential, we wait on doneDfd
    userDfd = doneDfd.then(createRandomUser);
    commentsDfd = addCommentsForUser(userDfd);

    // The result Promise for this iteration is then stored back in doneDfd
    doneDfd = Q.all([userDfd, commentsDfd]);
}

doneDfd.then(function() {
    console.log("All done!");
}, function(err) {
    console.error("Everything has broken with the error: ", err);
});
This is certainly not as clean as a synchronous language -- but can you imagine doing all of this sequentially using callbacks?

Accepting a Promise as an argument

from bin/make_fake_data

function addCommentsForUser(userDfd) {
    // Before we can add the comments, we need userDfd to resolve first.
    // It is possible it may already be resolved, so we use Q.when which
    // will make it a promise -- if it's a value, the promise will be immediately
    // resolved.
    return Q.when(userDfd).then(function(user) {
        var savedObjects = [];
        var comment;
        for (var i = 0; i < 10; ++i) {
            comment = new Comment();
            comment.user = user;
            comment.text = "I " + randgen(verbs) + " " + randgen(first_names);
            savedObjects.push(Q.nbind(comment.save, comment)());
        }
        return Q.all(savedObjects);
    });
}
Note that userDfd (dfd is a prefix I use, short for "Deferred", another name for a Promise) might or might not be a Promise.  If it is a resolved User object this will work exactly the same.

A promise is returned to indicate that all 10 comments were added.

sendResponse

an npm module that understands promises

https://github.com/taxilian-promises/sendresponse

sendResponse was adapted from a library we use at GradeCam

This code comes from express-demo in routes/rest.js
router.get('/users', function(req, res) {
    var userList = User.find().exec();
    res.sendResponse(userList);
});

router.get('/users/:email', function(req, res) {
    var user = getUserByEmail(req.params.email);
    res.sendResponse(user);
});

router.get('/users/:email/comments', function(req, res) {
    var userDfd = getUserByEmail(req.params.email);
    var comments = getCommentsForUser(userDfd);

    res.sendResponse(comments);
});

Rules for making promises

  • Don't make a promise you don't intend to keep.
  • Trust, but verify!
  • For every promise, there is price to pay. ~Jim Rohn
  • The power of empty promises.
  • A promise made is a promise kept.

Rules for making promises

Don't make a promise you don't intend to keep
A promise should eventually settle (resolve or reject)!
// Wrong way!
function getCoolStuff(someValue) {
    var deferred = Q.defer();
    setTimeout(function() {
        if (Something_cool_happened) {
            deferred.resolve(some_cool_value);
        }
    }, 500);
}

// Right way!
function getCoolStuff(someValue) {
    var deferred = Q.defer();
    setTimeout(function() {
        if (Something_cool_happened) {
            deferred.resolve(some_cool_value);
        } else {
            deferred.reject(new Error("Nothing cool happened"));
        }
    }, 500);
}

Rules for making promises

Trust, but verify!
Always Always Always handle the error case!
// Wrong way!
GetDBRecord().then(function(result) {
    // Do something cool with the result
});

// Right way!
GetDBRecord().then(function(result) {
    // Do something cool with the result
}, function(err) {
    // Panic!
});
Always handling the error case does not mean you must always handle it right away -- if you return the Promise (or return the result of .then) then a calling method might handle it.

The easiest way to get a hard-to-find error with Promises is having something fail when you aren't handling the error case.

Rules for making promises

For every promise, there is price to pay. ~Jim Rohn


Promises are generally pretty efficient, but there are things to keep in mind when using them:

  • Excessive chaining can cause excessive stack depth
  • While a promise exists, its resolution value exists
    • This can lead to memory leaks!
  • Even the small overhead of the function calls and event bus on a promise can add up if enough calls are made

Rules for making promises

The power of empty promises

There are times when the result of a promise is less important than whether or not the promise has settled -- or even whether it succeeded or failed!

function displayLoadingIndicator(promise) {
    showLoadingIndicator();
    var hide = function() { hideLoadingIndicator(); };
    Promise.when(promise).then(hide, hide);
}

Some libraries, such as jQuery Deferred, have a .always() shortcut for this

Rules for making promises

A promise made is a promise kept
function changeTheUser(user) {
    return Promise.when(user).then(function(user) {
        // The user is resolved!
    });
}
If you are willing to have a function return a promise, or if the output doesn't matter, you can easily make that function not care if it's a promise or not.

Most promise libraries have something similar to the ECMAScript when function, which will accept a Promise or a value and return a Promise that will settle the same way that Promise does.

In the case of a value it immediately resolves to the value.
This is extremely powerful!!

ECMAScript 6 Promises

    • Still in development -- last updated in the draft April 5, 2014
    • Not final, but implemented in Chrome and Firefox (latest versions)
    • Terminology: "fulfilled", "rejected", "pending".
      • "settled" means not pending.  "resolved" if it is either settled or "locked in" to match another promise.
    • Functions
      • Promise.all - accepts an array one or more Promises or values and returns a promise that is fulfilled when all passed promises are fulfilled. Fulfills to an array of those promises or rejects to the reason of the first promise that is rejected.
      • Promise.race - accepts one or more values and returns a promise that is settled in the same way as the first promise to settle of the passed promises.
      • Promise.reject / Promise.resolve - Resolves or rejects the promise
      • Promise.defer() - returns a Deferred object with .reject(), .resolve(), and .promise
        • Promise.defer().promise returns a Promise that will be settled by the Deferred object and is thenable

Promises/A+ Standard

An open standard for sound, interoperable JavaScript promises—by implementers, for implementers.

    • Widely accepted as "The Standard" for Javascript Promises
    • Implemented by the majority of javascript Promise libraries
    • Partially supported by most other libraries
      • Defines thenable as any object with a then method.
    • Defines three states: 'pending', 'fulfilled', 'rejected'
    • The then method must accept two arguments, onFulfilled and onRejected.
    • Any Promise library supporting the Promises/A+ standard will be interoperable by nature of common support for the then method.


The ECMAScript 6 standard seems to be Promises/A+ compliant.


Example libraries: Q, Bluebird, ayepromise, and many others

jQuery Deferred Objects

Widely available on the client

  • Comes free with jQuery, already used by all jQuery async methods
  • jQuery 1.8 made Deferred objects thenable , so generally interoperable with Promise/A+ libraries
  • Besides resolve and rejected states, supports an additional "progress" callback that can be triggered multiple times to give progress updates before it is settled.

Promises, Promises

By Richard Bateman

Promises, Promises

Slides / notes for my Promises, Promises talk at 2014 OpenWest

  • 3,716