Promises

Jenn Voss

@vossjenn

Promises provide a way to manage asynchronous callbacks.

A promise represents a future value,
such as the eventual result of an asynchronous operation.

Why should you use them?

At it's core, a Promise is an object with a .then() method

the .then() method registers callbacks that handle the eventual value, or the failure to retrieve that value.

Promise { then: function, catch: function }

Promises are similar to event listeners, except:

Promises will be native to JavaScript
in ES6

The spec is still evolving, and there are several different proposals:

  • Promises/A by Kris Zyp - "Thenables"
  • Promises/B by Kris Kowal - Opaque Q API
  • Promises/KISS by AJ ONeal
  • Promises/C has been redacted
  • Promises/D by Kris Kowal - "Promise-sendables", for interoperable instances of Promises/B.

However Promises/A+ is now the most widely accepted.

Browser support

Chrome Firefox (Gecko) Internet Explorer Opera Safari
32
+ Chrome for Android
30
25 as Promise
(partial, behind flag*)
24 as Future
Not yet supported 19 Not yet supported

Polyfill: https://github.com/jakearchibald/es6-promise

*Gecko 24 has an experimental implementation of Promise, under the initial name of Future. It got renamed to its final name in Gecko 25, but disabled by default behind the flag dom.promise.enabled. Even when this flag is enabled, some methods are still not supported, such as Promise.all() (as of Firefox 28)

Usage

var p = new Promise(function(resolve, reject) {
    resolve('success!');
});

p.then(function(val) {
   console.log(val); // logs "success!"
});

Remember, the point of promises is to give us functional composition and error bubbling in the async world. They do this by saying that your functions should return a promise, which can do one of two things:

Let's simulate some async code using setTimeout

var existingUsers = ['arnold'];

function createUser(user) {
    var userFound = existingUsers.indexOf(user) !== -1;

    return new Promise(function(resolve, reject) {
        if (userFound) {
            reject(
                user + ' is taken. Please choose another name.'
            );
        } else {
            existingUsers.push(user);
            setTimeout(resolve, 1000,
                'User ' + user + ' successfully created');
        }
    });
}
arnold hulk chuck mr_t
createUser('hulk')
    .then(function() {
        return createUser('chuck');
    }).then(function() {
        return createUser('mr_t');
    }).then(function(val) {
        console.log('all done.', val)
    }).catch(function(error) {
        console.log(error);
    });


/*

Logs: "all done. User mr_t successfully created"

Only the last resolved value is passed to .then()

*/

arnold hulk chuck mr_t
createUser('hulk')
    .then(function() {
        // arnold already exists,
        // so this will return a rejected Promise
        return createUser('arnold');
    }).then(function() {
        return createUser('chuck');
    }).then(function() {
        return createUser('mr_t');
    }).then(function(val) {
        console.log('all done.', val);
    }).catch(function(error) {
        console.log(error);
    });


/*

Logs: "arnold is taken. Please choose another name."

Successfully creates 'hulk', but never reaches 'chuck' or 'mr_t'

*/

Promise.all

arnold hulk chuck mr_t
Promise.all(
    [createUser('hulk'), createUser('chuck'), createUser('mr_t')]
).then(function(val) {
    // returns an array of each resolved value
    console.log(val);
}).catch(function(error) {
    // returns the first error encountered
    console.log(error);
});


/*

Logs: ["User hulk successfully created", "User chuck successfully created", "User mr_t successfully created"]

Waits for all of the Promises in the array to return a value before moving on

*/

arnold hulk chuck mr_t

Promise.race

Promise.race(
    [createUser('hulk'), createUser('chuck'), createUser('mr_t')]
).then(function(val) {
    console.log(val);
}).catch(function(error) {
    console.log(error);
});


/*

Logs: "User hulk successfully created"

All of the Promises in the array get called, but only the first value returned gets passed to .then() or .catch()

*/


arnold hulk chuck mr_t

Error handling

createUser('hulk')
    .then(function(){
        return createUser('arnold');
    }).then(
        function(){
            return createUser('chuck');
        },
        function(err) {
            // log this error and continue down the chain
            console.log('Silent error:', err);
            return 'fixed';
        }
    )
    .then(function(){
        return createUser('mr_t');
    }).then(function(val){
        console.log('all done.', val)
    }).catch(function(error){
        console.log(error);
    });


/*

Logs:
"Silent error: arnold is taken. Please choose another name."
"all done. User mr_t successfully created"

user 'chuck' does not get created
.catch() is never called

*/

What about jQuery?

jQuery has a $.Deferred object, which is really just a handler for a Promise object.

It allows you to access some of the benefits of Promises and provides a variety of custom methods for handling resolved or rejected values.

However, it does not conform to the Promises/A+ spec and suffers from two major issues*:

  • it allows multiple fullfillment values and rejection reasons
  • it does not pass error objects into rejections. When an error is thrown, code execution will stop.

*As of v2.1 beta

jQuery $.Deferred()

Methods

Here's our previous function rewritten using $.Deferred()

function createUser(user) {
    var userFound = existingUsers.indexOf(user) !== -1,
        $dfd = $.Deferred();

    if (userFound) {
        $dfd.reject(
            user + ' is taken. Please choose another name.'
        );
    } else {
        existingUsers.push(user);
        setTimeout($dfd.resolve, 1000,
            'User ' + user + ' successfully created');
    }
    return $dfd.promise();
}
arnold hulk chuck mr_t
createUser('hulk')
    .then(function(){
        return createUser('chuck');
    }).then(function(){
        return createUser('mr_t');
    }).then(function(val){
        console.log('all done.', val)
    }).fail(function(error){
        console.log(error);
    });


/*

Logs: "all done. User mr_t successfully created"

jQuery uses .fail() instead of .catch()

*/

arnold hulk chuck mr_t
createUser('hulk')
    .then(function(){
        return createUser('arnold');
    }).then(
        function(){
            return createUser('chuck');
        },
        function(err) {
            // log this error and continue down the chain
            console.log('Silent error:', err);
            return 'fixed';
        }
    )
    .then(function(){
        return createUser('mr_t');
    }).then(function(val){
        console.log('all done.', val)
    }).fail(function(error){
        console.log('Error:', error);
    });


/*

Logs:
"Silent error: arnold is taken. Please choose another name."
"Error: fixed"

The value 'fixed' is passed directly to .fail(), rather than the next .then(). This results not only in a confusing error message, but user 'mr_t' is also never created

*/

How to fix jQuery's error handling problem

If your code throws an Error object anywhere, you're out of luck.

However it is possible to fix the rejection handler issue.

You can use another Promise library (such as Q) and wrap the function so that it returns a true promise:

var promise = Q.when($.get("https://github.com/kriskowal/q"));

Or, you can explicitly return a new $.Deferred() each time.

arnold hulk chuck mr_t
createUser('hulk')
    .then(function(){
        return createUser('arnold');
    }).then(
        function(){
            return createUser('chuck');
        },
        function(err) {
            var dfd = $.Deferred();
            console.log('Silent error:', err);

            dfd.resolve(err);
            return dfd.promise();
        }
    )
    .then(function(){
        return createUser('mr_t');
    }).then(function(val){
        console.log('all done.', val)
    }).fail(function(error){
        console.log('Error:', error);
    });


/*

Logs:
"Silent error: arnold is taken. Please choose another name."
"all done. User mr_t successfully created"

*/

Performance

Callbacks are inherently faster than Promises, however in most use cases the difference will be neglible.

If you need to perform hundreds of operations per second, then you may want to consider your options.

Promise library performance:

Promise library performance

via: http://complexitymaze.com/2014/03/03/javascript-promises-a-comparison-of-libraries/
and jsPerf

ES6 introduces two more new features:

Generators

Generators are functions which can be exited and later re-entered. Their context (variable bindings) will be saved across re-entrances.

The yield operator

yield suspends operation of the current function

function* addOne() {
    var i = 0;
    while(true) {
        yield i++;
    }
};

var gen = addOne();

console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2

Generators + Promises = task.js

"task.js is an experimental library for ES6 that makes sequential, blocking I/O simple and beautiful, using the power of JavaScript's new yield operator."
spawn(function*() {
    var data = yield $.ajax(url);
    $('#result').html(data);
    var status = $('#status').html('Download complete.');
    yield status.fadeIn().promise();
    yield sleep(2000);
    status.fadeOut();
});

task.js works with any framework or library that uses the Promises/A spec.

Questions?



Jenn Voss

@vossjenn

Work:

SunGard (we're hiring!)

Slides:

http://jennvoss.com/promises

Resources:

http://promisesaplus.com/

http://www.html5rocks.com/en/tutorials/es6/promises/

http://domenic.me/2012/10/14/youre-missing-the-point-of-promises/

http://complexitymaze.com/2014/03/03/javascript-promises-a-comparison-of-libraries/

http://thanpol.as/javascript/promises-a-performance-hits-you-should-be-aware-of/