Presentation I gave to the node.dc meetup group March 13, 2013 on using Promises and the Q library to make flow of control easier to reason about in Javascript code using async and callbacks
What is a Promise?
The simplest explanation: it is an easy way to avoid writing the Pyramid of Doom
Pyramid of Doom
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
}); // from https://github.com/kriskowal/q
Why Promises? What about?
• async: http://github.com/caolan/async
• step: https://github.com/creationix/step
• flow-js: https://github.com/willconant/flow-js
• ...you get the point
Non-Promises Fix:
async.series({
normalize: function(callback){
// Bodies removed for brevity
}
, compose: function(callback){
}
, invert: function(callback){
}
// A bunch more stuff killed
}
, function (err, results)
{
if (!err ) {
err = new Error('Results did not contain a valid image buffer')
}
else {
callback(err, results.imageBuffer);
}
}
}); //https://github.com/jonathana/heatNode/blob/master/lib/imageGen/generateimages.js
Is That Not Good Enough?
var later = Q.nfcall(nodedc.wait_a_few_slides);
// or: we’ll come back to it
Promises: Longer Explanation
“A Promise is an object representation of an event. In the course of its
life, a Promise goes from a pending state, when it’s called, to a resolved or
rejected state, when it’s been completed, or it could also stay pending
forever and is never resolved.”
http://flaviocopes.com/deferred-and-promises-in-javascript/
Say What?
• Promises take a call to an asynchronous function and wrap it with an
object whose methods proxy when the wrapped function either
completes or errors
• A good Promise library also provides a set of control methods on that
object wrapper to handle composition of multiple Promise-ified
asynchronous method calls
• Promises use the best qualities of an object--encapsulation of state--to
track the state of an asynchronous call
Promises provide a
solid abstraction
for representing the
state of an asynchronous call
and writing flow of
control code based on that state
Pyramid of Doom Again
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
}); // from https://github.com/kriskowal/q
Pyramid of Doom on Promises
Q.fcall(step1) // This returns a Promise obj
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
})
.done(); // from https://github.com/kriskowal/q
Again, Why Promises?
• It’s a spec: http://wiki.commonjs.org/wiki/Promises/A
• Generally supported by a bunch of libs both browser and server-side:
• jQuery (sort of, supposedly doesn’t fully work like Promises/A)
• AngularJS
• Q library (https://github.com/kriskowal/q)
• Provides separation of concerns between wrapping and flow of control
handling of deferred activities
Sharing
• Many libraries can exchange Promise objects
• AngularJS’ Promise API is explicitly based off a subset of Q. If Q is
loads first, AngularJS just uses that
• jQuery’s promises can be consumed by Q, with some adaptations
So Why Q?
• Q consumes Promises from most other libraries
• Pure Javascript library
• Can be used both client-side (browser or e.g. Phonegap) and server-side (npm install
q, but you’re using package.json, right?)
• Provides utilities to make it easy to write Promise-based code or wrap non-Promise-
based functions
• Provides a library of methods to build control flow around Promise results and errors
• Small (enough?): ~1400 SLOC, ~ 8.5kb minified
Q.fcall/nfcall: Wrap an Async
method with a Promise
function writeError(errMessage) {
return Q.nfcall(fs.writeFile, "errors.log",
errMessage);
}
• nfcall: node function call. Sort of a misnomer, original intent was to
make it easy to wrap node library/module calls into Promises, but
works for any async call
• fcall: turns functions synchronously returning a value into a Promise,
• You can make it so it’s Promises all the way down (at least until you hit
the turtles)
Q.defer: Interject Promise Support
• Use when you have to intermingle other logic inside callback work
function getLocation() {
var deferred = Q.defer();
console.log("Calling getCurrentPosition");
navigator.geolocation.getCurrentPosition(function(position) {
deferred.resolve(position);
console.log("getCurrentPosition resolved");
}, function(error){
deferred.reject(error);
console.log("getCurrentPosition errored");
});
return deferred.promise;
};
Q.when: Wrapping Other
Libraries’ Promises
• From the Q documentation: “Not all promise libraries make the same
guarantees as Q and certainly don’t provide all of the same methods.
Most libraries only provide a partially functional then method.”
return Q.when($.ajax(...))
.then(function () {
});
Simple Flow
• Use then/fail (and .done() to avoid swallowing unhandled exceptions)
• You can chain then calls
Q.fcall(step1) // This returns a Promise obj
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
})
.done(); // from https://github.com/kriskowal/q
More Complex Handling
• Q.reduce(): Chain indeterminate-length sequential chains
• all(): Turn multiple Promises in an array into a single Promise. Fails at
the first failure from any Promise, returning that failure
• allResolved(): Turn multiple Promises in an array into a single Promise.
Succeeds when all Promises complete, resolved or failed, and resolves
with the array of Promises
Mocha + Chai + Chai-as-Promised
• Adds testability methods to promises supporting BDD
• Note the use of done, which signals the async part of mocha
it('Should error on short barcode format', function(done){
var promise = lookupBarcode(lookupData.shortGtinData.gtin);
promise.should.be.rejected.and.notify(done);
});