PromisesAsynchronous Control Flow
A long time ago in a galaxy far far away...
The problemQ: How to control the asynchronous flow of your
application when there are dependencies between
two steps?
A: I can use Callbacks :)
function asyncFunction1(function (err, result) {// do something here...asyncFuncion2(function (err2, result2) {
// other stuff here...})
})
However… Callbacks can get uglymodule.exports.verifyPassword = function(user, password, done) {
if(typeof password !== ‘string’) {done(new Error(‘password should be a string’))
}
computeHash(password, user.passwordHashOpts, function(err, hash) {if(err) {
done(err)}
done(null, hash === user.passwordHash)})
}
Callback being called multiple times
This one is easily fixed though...module.exports.verifyPassword = function(user, password, done) {
if(typeof password !== ‘string’) {return done(new Error(‘password should be a string’))
}
computeHash(password, user.passwordHashOpts, function(err, hash) {if(err) {
return done(err)}
return done(null, hash === user.passwordHash)})
}
Always return when calling the callback
Q: How execute asynchronous function in
parallel and proceed when all have finished?
But what about parallel execution?var finished = [false, false]function asyncFunction1(function (err, result) { // do some stuff
finished[0] = true
if (finished[0] === true && finished[1] === true) {// proceed…
}})
function asyncFunction2(function (err, result) { // do some other stuff
finished[1] = trueif (finished[0] === true && finished[1] === true) {
// proceed…}
})
The callback hell
Find a better way you must...
The good ol’ async¹ moduleasync.waterfall([ function(callback) { callback(null, 'one', 'two'); }, function(arg1, arg2, callback) { // arg1 = 'one' and arg2 = 'two' callback(null, 'three'); }, function(arg1, callback) { // arg1 = 'three' callback(null, 'done'); }], function (err, result) { // result now equals 'done'});
async.parallel([ function(callback){ setTimeout(function(){ callback(null, 'one'); }, 200); }, function(callback){ setTimeout(function(){ callback(null, 'two'); }, 100); }],// optional callbackfunction(err, results){ // the results array will equal ['one','two'] even though // the second function had a shorter timeout.});
¹ https://github.com/caolan/async
But it can get cumbersome too...What if I need to pass an argument to the first
function in the waterfall?
async.waterfall([ async.apply(myFirstFunction, 'zero'), mySecondFunction, myLastFunction,], function (err, result) { // result = 'done'});
function myFirstFunction(arg1, callback) { // arg1 now equals 'zero' callback(null, 'one', 'two');}function mySecondFunction(arg1, arg2, callback) { // arg1 = 'one' and arg2 = 'two' callback(null, 'three');}function myLastFunction(arg1, callback) { // arg1 = 'three' callback(null, 'done');}
DRY*Error handling can be tiresome…
You have to bubble your errors up in every layer of
code you have.
And if you forget doing so, a wild bug may appear...
* Don’t Repeat Yourserlf
async.waterfall([ function(callback) { doSomething(function(err, result){ if (err) return callback(err) callback(null, result, 'another-thing'); }) }, function(arg1, arg2, callback) { doAnotherStuff(function(err, result){ if (err) return callback(err) callback(null, result); }) }, function(arg1, callback) { doAnotherStuff(function(err, result){ if (err) return callback(err) callback(null, result); }) }], function (err, result) { // result now equals 'done'});
Bug used “confusion”.
It was very effective.
Promises F.T.W.
What is a promise?The core idea behind promises is that it represents the result of an asynchronous
operation. A promise is in one of three different states:
- Pending: The initial state of a promise.
- Fulfilled: The state of a promise representing a successful operation.
- Rejected: The state of a promise representing a failed operation.
How does a promise work?The State Diagram of a promise is as simple as this one:
Clarifying a little bit- When pending, a promise:
○ may transition to either the fulfilled or rejected state.
- When fulfilled, a promise:
○ must not transition to any other state.
○ must have a value, which must not change.
- When rejected, a promise:
○ must not transition to any other state.
○ must have a reason, which must not change.
The “then” methodThe “then” method is called when a promise is
either fulfilled or rejected.
This two conditions are treated by different
callbacks:
promise.then(onFulfilled, onRejected)
- onFulfilled(value): value of fulfilment of the
promise as its first argument
- onRejected(reason): reason of rejection of
the promise as its first argument. This
argument is optional.
functionReturningPromise.then(function(value){// do something with value
}, function (reason) {// do something if failed
})
Promise chainingThe “then” method always returns a new promise,
so it can be chained.
Even if your callbacks return a value, the promise
implementation will wrap it into a brand new
promise before returning it.
functionReturningPromise.then(function(value){ return 10}) .then(function (number) { console.log(number) // 10 }) .then(...) .then(...) .then(...) .then(...) .then(...)// The chaining can go indefinetly
Error handlingIf any error is thrown within a then callback, a
rejected promise will be automatically returned
with the error as its reason.
The immediately after then call will only execute
the onRejected callback.
If onRejected is not provided or is not a function,
the error will be passed to the next chained then.
If no then call in the chain have a onRejected
callback, the error will be thrown to the main code.
functionReturningPromise.then(function(value){ throw new Error(‘Something bad happened’)}) .then(function (someArg) { // do nothing }, function (reason) { console.log(reason) // Error: Something … return 42 // Will return a resolved promise })
Error bubblingIf onRejected is not provided or is not a function,
the error will be passed to the next chained then.
If the next chained then has the onRejected
callback, it will be called, giving you a possibility of
dealing with the error.
IMPORTANT: if you don’t throw a new error or
re-throw the error argument, the then method will
return a promise that is resolved with the return
value of the onRejected callback.
If no then call in the chain have a onRejected
callback, the error will be thrown to the main code.
functionReturningPromise.then(function(value){ throw new Error(‘Something bad happened’)}) .then(function (number) { console.log(number) // Will be bypassed }) .then(function (someArg) { // do nothing }, function (reason) { console.log(reason) // Error: Something … // Will return a resolved promise }) .then(function (number) { console.log(number) // undefined because the previous // callback doesn’t return a value })
Error catchingIn most situations, you only want to deal with
possible errors once.
You can do this by adding a then call at the end of
your chain with only the onRejected callback.
This way, any subsequent then call after the error
throwing will be bypassed and the error will only
be handled by the last one.
Since the last then call is only for error catching,
you don’t need to set a onResolved callback and may
use null instead.
functionReturningPromise.then(function(value){ throw new Error(‘Something bad happened’)}) .then(function (...) { // Will be bypassed }) .then(function (...) { // Will be bypassed }) .then(function (...) { // Will be bypassed }) .then(null, function (error) { // Error: Something … console.log(error) })
Promises A+
StandardizationPromise A+¹ is a standard specification for promise
implementations.
It allows interoperability between different
promise libraries.
You don’t need to worry about what
implementation of promise a 3rd party module
uses, you can seamlessly integrate it into your code,
as long as it respects the A+ specs.
The specs are at the same time powerful and dead
simple: less than 100 lines of text in plain English.
¹ https://promisesaplus.com/
someLibrary.methodReturningPromise() .then(function (result) { return anotherLibrary.anotherPromise() }) .then(function (anotherResult) { // ... })
I mean, a de facto standard...Promises are now part of the core¹ of EcmaScript 6
(ES6).
That means it is available as part of the standard
library of modern Javascript engines:
● Node.js >= v0.12.*
● Any modern and updated browser (Firefox,
Chrome, Safari, Edge, etc. -- NOT IE)
¹ https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise
² http://caniuse.com/#feat=promises
²
Enhancing Promises
Parallel executionMost of the promise implementations (including
the core one in ES6) have some extra methods will
allow you to have asynchronous code running in
parallel.
The Promise.all¹ method will take an array of
promises as an argument and will return a promise
that will only be resolved when and if all the input
promises are resolved.
The Promise.race¹ method will also take an array of
promises, but will return a promise that resolves to
the value of the first of the input to be resolved.
Promise.all([promise1, promise2, promise3]) .then(function (result) { // result will be an array with the values // of the input promises. // The order is preserved, so you’ll have: // [resPromise1, resPromise2, resPromise3] })
Promise.race([promise1, promise2, promise3]) .then(function (result) { // result will be the either resPromise1, // resPromise2 or resPromise3, depending // on which promise finishes firt })
¹ Not part of the Promise A+ specification
The catch methodThe catch¹ method is just a syntactic sugar around
the last-then-treats-error pattern discussed before.
functionReturningPromise.then(function(value){ throw new Error(‘Something bad happened’)}) .then(null, function (error) { console.log(error) })
// Can be rewritten as following in most// implementations of Promise:
functionReturningPromise.then(function(value){ throw new Error(‘Something bad happened’)}) .catch(function (error) { console.log(error) })
¹ Not part of the Promise A+ specification
Creating settled promisesAnother syntactic sugar allows you to create
promises that are already fulfilled or rejected,
through the helper methods Promise.resolve and
Promise.reject.
This can be very useful in cases that your interface
states you should return a promise, but you already
have the value to return (ex.: caching).
var cache = {}
functionReturningPromise.then(function(value){ throw new Error(‘Something bad happened’)}) .then(function (result) { if (cache[result] !== undefined) { return Promise.resolve(cache[result]) } else { return getFreshData(result) .then(function (data) { cache[result] = data return data )} } })
¹ This is not part of the Promise A+ specification
Non-standard¹ cool extrasThe finally method allows you to have a callback
that will be executed either if the promise chain is
resolved or rejected.
The tap method is a really useful syntactic sugar
that allows you to intercept the result of a promise,
but automatically passing it down the chain.
The props method is like Promise.all, but resolves
to an object instead of an array.
db.connect() // Implemented using bluebird .then(function() { // run some queries }) .finally(function () { // no matter what, close the connection db.disconnect() })
var Promise = require('bluebird')Promise.resolve(42) // will print 42 into the console and return it .tap(console.log.bind(console)) .then(function (result) { // result is still 42 })
Promise.props({ a: getA(), b : getB()}) .then(function (obj) { // Will print { a : …, b : … } console.log(obj) })
¹ Based on Bluebird (http://bluebirdjs.com/docs/) -- a full-featured promise implementation
Promisify all the things!
Converting callback-based codeBluebird¹ has some utility methods that will adapt
callback-based functions and libs to use promises.
The Promise.promisify method will take any
error-first callback and return a promise that will
be resolved if the callback is called without error or
rejected otherwise.
The Promise.promisifyAll method will take an
objects and iterate over all it’s methods and create
a new implementation of them, keeping the same
name, suffixed by “Async”.
Var Promise = require('bluebird’)var fs = require('fs')var readFileAsync = Promise.promisify(fs.readFile)
readFileAsync('someFile.ext') .then(function (contents) { // do something })
// or...
Promise.promisifyAll(fs)
fs.readFileAsync('someFile.ext') .then(function (contents) { // do something })
¹ http://bluebirdjs.com/
A-a-any d-d-doubts?
@hjpbarcelos
henriquebarcelos
Henrique José Pires Barcelos
Fullstack Software Engineer @ Revmob
About the author