JavaScript Promises


Are super awesome.  And you should use them.





http://slides.com/adamterlson/promises
http://github.com/adamterlson/promises-presentation



First: A Complete History of Javascript







Just kidding.



The Event Loop

No Threads, Async By Nature

  • JavaScript is single-threaded
  • The Event Loop
  • Each message is processed completely before any other message is processed.
  • Never blocks
 

Async methods add to 
the event queue


var result = null;
$.get('/echo/json', function (resp) {
    result = resp;
});

console.log('result: ', result);

// result: null
Each message is processed completely 
before any other message is processed.


Dealing with Async via Callbacks

Single Message

Pretty easy

$.get('...', function (resp) {
    // Use the single response
});

Sequential Messages

Getting harder...

function getPart1(callback) {
   $.get('...', callback);
}

function getPart2(callback) {
   $.get('...', callback);
}

function getPart3(callback) {
   $.get('...', callback);
}

getPart1(function () {
    getPart2(function () {
        getPart3(function () {
            // Add value
        });
    });
});

Parallel Messages

Kill me now
var responses = [];
function somethingResponded(resp) {
    responses.push(resp);
    
    if (responses.length === 3) {
        someOtherFunction(responses);
    }
}   
function getPart1(callback) {
    $.get('firstURL', callback);
}
function getPart2(callback) {
    $.get('secondURL', callback);
}
function getPart3(callback) {
    $.get('thirdURL', callback);
}
getPart1(somethingResponded);
getPart2(somethingResponded);
getPart3(somethingResponded);

welcome to

CALLBACK HELL

app.get('/', function(req, res) {
    // Open the connection.
    db.open(function(err, db) {
        // Get one collection.
        db.collection('users', function(err, usersColl) {
            // Search the first collection.
            usersColl.find({}, {'limit': 3}, function(err, usersCursor) {
                // Convert the result into an array.
                usersCursor.toArray(function(err, users) {
                    // Get the second collection.
                    db.collection('articles', function(err, artColl) {
                        // Search the second collection.
                        artColl.find({}, {'limit': 3}, function(err, artCursor) {
                            // Convert the result into an array.
                            artCursor.toArray(function(err, articles) {
                                // Now we have two arrays, users and articles, in scope.
                                // Render the home page.
                                res.render('home.ejs', {'users': users, 'articles': articles});



The Problem of Control

In Callback Hell, callback execution 

is managed by your unit

function populateModel(model, callback) {
    // Make sure callback is defined
    if (!callback) callback = function () { };
    
    $.get('/api/model', {
        success: function (data) {
            model.data = data;
            callback(null, data);
        },
        error: function (err) {
            // Have to always call callback, maybe it needs to happen
            // but maybe it'll break if we do, so we need to go to
            // node-style callbacks everywhere
            callback(err);
        }
    });
}
populateModel(modelA, function () { console.log(modelA.data); });
populateModel(modelB);
populateModel owns logic for callback execution
Unit testing this sucks

What about error handling?


In Callback Hell every callback must handle its own error cases

fs.readdir('/some/path', function (err, fileNames) {
    // Often forgotten or ignored: handling err
    if (err) throw "We're screwed";
    
    fileNames.forEach(function (name) {
        fs.readFile(name, function (fileErr, file) {
            // Especially when doing nested or more complex flows
            if (fileErr) throw "This is getting seriously annoying";
            
            // Add real value here
        });
    });
});

The solution:




Re-invert the process control

Promises



Promises/A Spec


A promise is defined as an object that has a function as the value for the property then:
then(fulfilledHandler, errorHandler, progressHandler)
Adds a fulfilledHandler, errorHandler, and progressHandler to be called for completion of a promise. The fulfilledHandler is called when the promise is fulfilled. The errorHandler is called when a promise fails. The progressHandler is called for progress events. All arguments are optional and non-function values are ignored. The progressHandler is not only an optional argument, but progress events are purely optional. Promise implementors are not required to ever call a progressHandler (the progressHandler may be ignored), this parameter exists so that implementors may call it if they have progress events to report.
This function should return a new promise that is fulfilled when the given fulfilledHandler or errorHandler callback is finished. This allows promise operations to be chained together. The value returned from the callback handler is the fulfillment value for the returned promise. If the callback throws an error, the returned promise will be moved to failed state.

Basically....

  • Promises expose a function (then) to transform the state of the promise
    • then returns a new promise
    • allows for the attachment of handlers that will be executed based on state
    • handlers are guaranteed to execute in order attached
  • Once fulfilled or rejected, a promise's state never changes
  • Promises become fulfilled by a value
  • Promises get rejected by exceptions



Simple workflows 
with promises







.then(onDone, onFail)

Single Method

    $.get('...').then(function () {
        // Success Method
    });

Sequential Execution

function getPart1() {
    return $.get('...');
}

function getPart2() {
    return $.get('...');
}

function getPart3() {
    return $.get('...');
}

getPart1()
    .then(getPart2)
    .then(getPart3);






$.when()

Q.all()

Parallel Execution


in jQuery

$.when($.get(...), 2, $.get(...), 4)
    .then(function (one, two, three, four) { })

Q

Q.all([promise1, 2, promise3, 4]).then(function (arrResults) { } )


Non promises are treated as resolved promises of that value.

Parallel Messages

in jQuery
function getPart1() {
    return $.get('...');
}

function getPart2() {
    return $.get('...');
}

function getPart3() {
    return $.get('...');
}

$.when(getPart1(), getPart2(), getPart3())
    .then(function () {
        console.log('All done!');
    });
in Q
Q.all([getPart1(), getPart2(), getPart3()])
    .then(function () {
        console.log('All done!'); 
    });



Constructing

promises

What is a Deferred?


.resolve(value) // Means to resolve
+
.reject(reason) // Means to reject
+
.promise() // Means to construct promise
+
Other useful stuff

Promise Construction

in jQuery
function resolveAfterOneSecond(message) {
    var dfd = $.Deferred();
    
    setTimeout(function () {
        dfd.resolve(message);
    }, 1000);
    
    return dfd.promise();
}   
in Q
function resolveAfterOneSecond(message) {
    var dfd = Q.defer();
    
    setTimeout(function () {
        dfd.resolve(message);
    }, 1000);
    
    return dfd.promise;
}   




"Promises become fulfilled 

with a value"

Resolving after construction


var dfd = Q.defer(),
    promise = dfd.promise;
    
dfd.resolve('Yay Happy!');

promise.then(function (result) {
    console.log(result); // Yay Happy!
});

Returning values 
in workflows

Returning a value resolves the promise with that result, passed to the handler
var promise = Q(10)
    .then(function (num) {
        return num+1;
    }).then(function (num) {
        console.log(num); // 11
    });

promise.then(function (num) {
    console.log(num); // undefined!
});

Returning promises in workflows


Return another promise and the resultant value of the 
'parent' promise becomes that of the returned 'child' promise.

var root = Q(10);

var parent = root.then(function () {
    var child = Q(20);

    return child;
});

parent.then(function (num) {
    console.log(num); // 20
});

The parent is resolved and rejected 
with the child with the same values at the same time.




"Promises are rejected by exceptions"



When constructed manually

var dfd = Q.defer();
dfd.reject(new Error('Thank you Todd')); // Can reject with any value


Unexpectedly in a workflow

var promise = Q()
    .then(someFunction)
    .then(function bomber() {
        throw new Error('Unexpected!');
    })
    .then(notcalled)
    .then(stillnotcalled);
    
console.log(promise.isRejected()); // True
    

Handling Exceptions in Workflows


promise
    .then(doSomething)
    .then(doSomethingElse, function errorHandler(err) {
        console.log('You experienced an error!');
    });
});

// In Q only:

promise
    .then(doSomething)
    .then(doSomethingElse)
    .catch(function errorHandler(ex) {
      console.log('You experienced an error!');
    });
});

This is broken in jQuery


var dfd = $.Deferred();

dfd
    .then(function () {
        throw new Error('Unexpected');
    }).then(
        function done() {
            // ...
        }, 
        function fail() {
            console.log('sad, but happy'); // <---- Not this one.
        }
    );

try {
    dfd.resolve();
} catch (ex) {
    console.log('THE WORST');  // <---- This one.
}
But if you already have it at a dependency, it's good
 enough for in-browser promises

Regarding not handling errors in Q

When you:
  • Have a promise-based workflow
  • And the promise isn't used elsewhere
  • And you do not wish to handle any exceptions


Then end your workflow with  .done()
app.get('/user/:id', function (req, res) {
    getUser(req.params.id)
        .then(populateUserDocuments)
        .then(res.json) // json response helper
        .done(); // Will cause any unhandled exceptions to throw
});
"This method should be used to terminate chains of promises that will not be passed elsewhere. Since exceptions thrown in then callbacks are consumed and transformed into rejections, exceptions at the end of the chain are easy to accidentally, silently ignore. "



TEST TIME

What's Logged?

var promise1 = resolveAfterOneSecond('Hello'); // Makes a promise resolved with 'Hello'

var promise2 = promise1.then(function (resp) {
    console.log('A: ' + resp);
});
    

 // A: Hello
    

What's Logged?

var promise1 = resolveAfterOneSecond('Hello');

var promise2 = promise1.then(function (resp) {
    console.log('A: ', resp);
});

promise2.then(function (resp) {
    console.log('B: ', resp);
});
    

// A: Hello
// B: Undefined
    

What's Logged?

var promise1 = resolveAfterOneSecond('Hello');

var promise2 = promise1.then(function (resp) {
    console.log('A: ' + resp);

    return "Goodbye";
});

promise2.then(function (resp) {
    console.log('B: ' + resp);
});

// A: Hello
// B: Goodbye

What's Logged?

var promise1 = resolveAfterOneSecond('Hello');

var promise2 = promise1.then(function (resp) {
    console.log('A: ', resp);

    return resolveAfterOneSecond('Goodbye');
})

promise2.then(function (resp) {
    console.log('B: ', resp);
});

// A: Hello
... One second later
// B: Goodbye  

What's Logged?

var promise1 = resolveAfterOneSecond('Hello'); // Makes a promise resolved with 'Hello'

var promise2 = promise1.then(function (firstResp) {
    return resolveAfterOneSecond('Goodbye')
        .then(function (secondResp) {
            return firstResp + ' ' + secondResp;        });
});

promise2.then(function (resp) {
    console.log('A: ', resp);
});

// A: Hello Goodbye


What's Logged?

var promise1 = resolveAfterOneSecond('Hello');

var promise2 = promise1
    .then(function (resp) {
        throw 'Whoopsies!'
    });

promise2.then(function (resp) {
    console.log('A: ' + resp);
});

// Nothing!


A typical callback workflow

function entireWorkflow(callback) {
    fs.readdir('/some/path', function (err, fileNames) {
        // Often forgotten or ignored is handling err
        if (err) throw "We're screwed";
        
        fileNames.forEach(function (name) {
            fs.readFile(name, function (fileErr, file) {
                // Especially when doing nested or more complex flows
                if (fileErr) throw "This is getting seriously annoying";
                
                // Add real value here
            });
        });
        
        // Uhh... when do I execute callback?
    });
}

With promises

var readdir = Q.denodeify(fs.readdir),
    readfile = Q.denodeify(fs.readFile);
    
function entireWorkflow() {
    return readdir('/some/path')
        .then(function (fileNames) {
            // Convert array of file names into array of promises
            var arrPromises = fileNames.map(function (name) {
                // returned objects to push into the new array
                return readfile(name).then(function (file) {
                    // ...
                });
            });

            return Q.all(arrPromises);
        }).catch(function (ex) {
            // Exception handling for entire process
        });
}
Easily-implemented parallel optimization
Sane exception handling for the entire flow




Live Refactoring


http://github.com/adamterlson/promises-presentation




Promises Are NOT simply callback aggregators!


That's easy.





Promises bring back to asynchronous code what we have in synchronous code


Functional Composition

You can feed the result of one operation directly into another operation (impossible with callbacks).  Inherent re-inversion of control involved.


Error Bubbling

One function in the composition chain can throw an exception, which will bypass all further operations until it comes to a catch handler.  No need to re-throw (which is very dangerous) errors or handle every case.

Reads like synchronous

    // Entirely async
    getDocument()
        .then(function (document) {
            var status = determineStatus(document);
            return validatePublishReady(status);
        })
        .then(publishDocument)
        .then(function (publishResponse) {
            console.log("Response: " + publishResponse);
        })
        .catch(function (err) {
            console.log("Publishing error: " + err);
        });
    // Entirely blocking
    try {
        var document = getDocument();
        var status = determineStatus(document);
        var publishResponse = publishDocument(validatePublishReady(status));
        
        console.log("Response: " + publishResponse);
    } 
    catch(err) {
        console.log("Publishing error: " + err);
    }

Control re-inversion

Callbacks:
function populateModel(model, callback) {
    // Make sure callback is defined
    if (!callback) callback = function () { };
    
    $.get('/api/model', {
        success: function (data) {
            model.data = data;
            callback(null, data);
        },
        error: function (err) {
            // Have to always call callback, maybe it needs to happen
            // but maybe it'll break if we do, so we need to go to
            // node-style callbacks everywhere
            callback(err);
        }
    });
}

populateModel(modelA, function () { console.log(modelA.data); });
populateModel(modelB);

Control re-inversion

Promises:
function populateModel(model) {
    // What happens next is out of scope
    // Clean unit for testing!
    
    return $.get('/api/model')
        .then(function (data) {
            model.data = data;
            return model;
        });
}

populateModel(modelA)
    .then(function (model) { console.log(model.data); });
populateModel(modelB); // Does nothing with the promise and that's okay!



When to use promises

When working with 

client-side AJAX and jQuery...


Never use success/error properties on $.ajax operations.  There is no benefit, only diminished utility and functional limitations.


Always use the Promise API

Node Apps...


Promises are a requirement to clean application workflows.

Use promises in internal workflows.

Distributed libraries, modules,
and packages...


Node-style callbacks are the defacto standard for distributed code APIs.

Limiting dependencies can be nice.

But supporting a promise-based API can make sense.

Use Q().nodeify()  to support both easily!

WHAT ABOUT ANGULAR?


Know when to use $http().then() and when to use $http.success()


When working with Backbone


All Backbone.Model and Backbone.Collection AJAX functions 
return the jqXHR object straight out of jQuery, which is itself a promise.

// Set the default implementation of `Backbone.ajax` to proxy through to `$`.
// Override this if you'd like to use a different library.
Backbone.ajax = function() {
  return Backbone.$.ajax.apply(Backbone.$, arguments);
};

Backbone.sync = function(method, model, options) {
  ...
    
  // Make the request, allowing the user to override any Ajax options.
  var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
  model.trigger('request', model, xhr, options);
  return xhr;
};
Backbone Source


Callbacks

<->

Promises

(In Node with Q)

From callback to promise

Q has a bunch of utility methods for this:

  • Q.nfapply(nodeFunc, args)
  • Q.nfcall(func, ...args)
  • Q.post(object, methodName, args)
  • Q.invoke(object, methodName, ...args)

Node-callback style has the 'n' preface

Q.nfcall(FS.readFile, "foo.txt", "utf-8").then(function (text) {
});

From promise to

node-style callback

module.exports = exports = function MyModule(someArg, callback) {
    return doSomethingAsync(someArg)
        .then(doSomethingElseAsync)
        .then(doAnotherThingAsync)
        .nodeify(callback); // Will execute callback in [err, result] fashion if present
}; 



Promises will be baked into the next version of JavaScript (ES6)



"Promises are so fundamentally useful that somehow Mozilla, Google, Apple, Microsoft, Adobe, Facebook, and others all agreed it was important enough to build into the language."

— A TC39 ECMAScript Committee member

What it looks like


Construction and resolution
var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

promise.then(function () {
  // success
}, function () {
  // failure
});
$.when and Q.all equivalent
Promise.all([somevalue, apromise]).then(function(values) {
  // values == [ true, 3 ]
});

And Node!




Unit Testing Promises

(in Mocha)

Without a helper library

Pass in done, and execute manually
describe('User', function () {
  describe('#save()', function () {
    it('should save without error', function (done){
      var user = new User('Luna');
      user.save().then(function (res) {
        // Assertions here
        done();
      }, function (err) {
        done(err);
      });
    })
  })
})

Making it easier

mocha-as-promised
it("should be fulfilled with 5", function () {
    // Return your promise, rather than executing a done callback
    return promise.then(function (result) {
        return result.should.equal(5);
    });
});
Chai assertions with chai-as-promised
it("should be fulfilled with 5", function () {
    return promise.should.become(5);
});


Links


Made with Slides.com