Why do we need promises? How does promises compare to a simple callback approach or modules like async? Are promises just a hype or a new standard for asynchronous code?
3. The problem
Q: 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...
})
})
4. However… Callbacks can get ugly
module.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
5. 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
6. 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] = true
if (finished[0] === true && finished[1] === true) {
// proceed…
}
})
9. The good ol’ async¹ module
async.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 callback
function(err, results){
// the results array will equal ['one','two']
even though
// the second function had a shorter timeout.
});
¹ https://github.com/caolan/async
10. 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');
}
11. 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.
13. 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.
14. How does a promise work?
The State Diagram of a promise is as simple as this one:
15. 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.
16. The “then” method
The “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
})
17. Promise chaining
The “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
18. Error handling
If 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
})
19. Error bubbling
If 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
})
20. Error catching
In 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)
})
22. Standardization
Promise 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) {
// ...
})
23. 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
²
25. Parallel execution
Most 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
26. The catch method
The 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
27. Creating settled promises
Another 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
28. Non-standard¹ cool extras
The 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
30. Converting callback-based code
Bluebird¹ 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/