Promises - Asynchronous Control Flow

Post on 24-Jan-2017

49 views 1 download

Transcript of Promises - Asynchronous Control Flow

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