Promises - Asynchronous Control Flow

33
Promises Asynchronous Control Flow

Transcript of Promises - Asynchronous Control Flow

Page 1: Promises - Asynchronous Control Flow

PromisesAsynchronous Control Flow

Page 2: Promises - Asynchronous Control Flow

A long time ago in a galaxy far far away...

Page 3: Promises - Asynchronous Control Flow

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...})

})

Page 4: Promises - Asynchronous Control Flow

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

Page 5: Promises - Asynchronous Control Flow

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

Page 6: Promises - Asynchronous Control Flow

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…}

})

Page 7: Promises - Asynchronous Control Flow

The callback hell

Page 8: Promises - Asynchronous Control Flow

Find a better way you must...

Page 9: Promises - Asynchronous Control Flow

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

Page 10: Promises - Asynchronous Control Flow

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');}

Page 11: Promises - Asynchronous Control Flow

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.

Page 12: Promises - Asynchronous Control Flow

Promises F.T.W.

Page 13: Promises - Asynchronous Control Flow

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.

Page 14: Promises - Asynchronous Control Flow

How does a promise work?The State Diagram of a promise is as simple as this one:

Page 15: Promises - Asynchronous Control Flow

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.

Page 16: Promises - Asynchronous Control Flow

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

})

Page 17: Promises - Asynchronous Control Flow

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

Page 18: Promises - Asynchronous Control Flow

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 })

Page 19: Promises - Asynchronous Control Flow

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 })

Page 20: Promises - Asynchronous Control Flow

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) })

Page 21: Promises - Asynchronous Control Flow

Promises A+

Page 22: Promises - Asynchronous Control Flow

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) { // ... })

Page 23: Promises - Asynchronous Control Flow

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

²

Page 24: Promises - Asynchronous Control Flow

Enhancing Promises

Page 25: Promises - Asynchronous Control Flow

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

Page 26: Promises - Asynchronous Control Flow

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

Page 27: Promises - Asynchronous Control Flow

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

Page 28: Promises - Asynchronous Control Flow

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

Page 29: Promises - Asynchronous Control Flow

Promisify all the things!

Page 30: Promises - Asynchronous Control Flow

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/

Page 31: Promises - Asynchronous Control Flow

A-a-any d-d-doubts?

Page 32: Promises - Asynchronous Control Flow
Page 33: Promises - Asynchronous Control Flow

@hjpbarcelos

henriquebarcelos

Henrique José Pires Barcelos

Fullstack Software Engineer @ Revmob

About the author