Promises

who to trust in the asynchronous world

@naugtur, meet.js 01/2015 //work in progress, visit again for more content

Do I really have to tell you about the callback hell?

Ok, there you go


db.set('key1', 'value1', function(err) {
    if(err) throw err;

    db.set('key2', 'value2', function(err) {
        if(err) throw err;

        db.set('key3', 'value3', function(err) {
            if(err) throw err;

            //...
        });
    });
});

                        

And now imagine you need error recovery too

Never had that kind of problem.

Maybe you called it a race condition or used a network of custom events?

A well hidden callback hell


someModel.on("fetch",function(){
    app.trigger("needToRedrawThatOtherThing")
})                        

app.on("needToRedrawThatOtherThing",function(){
    someOtherModel.reload()
    someOtherModel.on("error",function(){
        app.someModel.goBack()
    });
})                        

Looks more familiar? This has more than one problem in it.

What's a promise?

Some theory

A promise is basically a function that is going to
tell you the result some time soon.

But the important stuff is chaining and error propagation.

  • Promises/A - the initial spec
  • Promises/A+ - the same spec, but more words
  • All they define is some terminology
  • ... and a .then method

Spec: Promises/A+ [promisesaplus.com]

Important words

  • Resolve
  • Fulfilled
  • Rejected
  • Thenable

Ok, enough already

Time for code samples

What .then() does

It returns a new promise that is fulfilled by the functions in arguments


var newPromise = initialPromise.then(function(value){
    return 42 //value or another promise
}, function(){
    //initialPromise rejected
})

newPromise.then(function(value){
   //value === 42
})
                        

.__.     One promise with functions called on resolution
 \_.
                    

fetchSomething().then(fulfill, reject)
                        

fetchSomething().then(function(theThing){
    use(theThing)
}, function(error){
    console.log('oh crap...')
})
                        

Some syntactic sugar


.then(null, function(){})
                  

==


.catch(function(){})
.fail(function(){}) //for old browsers
                  

.__.__.     A chain with no error handling
 \  \
                  

a().then(b).then(c)
                        

This example is bad for your error handling


.__.__.     A chain with error handling, but not for c()
 \__\_.
                  

a().then(b).then(c,errorHandling)
                        

This example is bad for your error handling


.__.__.     A chain with all errors handled
 \__\__\_.
                  

a().then(b).then(c).catch(errorHandling)
                        

fetchSomething().then(function(theThing){
    return use(theThing)
}).then(function(){
    throw new Error("trololo")
}).catch(function(error){
    console.log('oh crap...')
})
                        

Important debugging tip

Always return the promise to some code that has error handling or finish it


function promiseStuff(){
    return fetchSomething().then(function(theThing){
        return use(theThing)
    })
}
a().then(promiseStuff).catch(...)
                        

or


fetchSomething().then(function(theThing){
    return use(theThing)
}).done() //ends a chain and throws errors if not yet handled
                        

some promise implementations will throw unhandled errors, but that's not in the standard


.__.__.     A chain with error recovery
 \_._/
                  

a().then(b, errorRecovery).then(recoveredFromError)
                        

fetchSomething().then(function(theThing){
    return parse(theThing)
},function(error){
    return backupValue
}).then(function(value){
    use(value)
})
                        

.__(if)__.     A chain with a conditional invocation

                  

a().then(conditionally).then(b)
                        

fetchSomething().then(function(theThing){
    if(!theThing){
        return fetchSomethingElse() //also a promise
    }
    return theThing //just a value
}).then(function(value){
    use(value)
})
                        

Examples with standard promises

Scoping


fetchSomething().then(function(ignoredValue){
    return fetchSomethingElse()
}).then(function(value){
    use(value)
}).catch(function(err){
    console.error(err);
})
                    

fetchSomething().then(function(theThing){
    return fetchSomethingElse().then(function(anotherThing){
        if(theThing === anotherThing){
            return theThing
        }else{
            throw new Error("No match")
        }
    })
}).then(function(value){
    use(value)
}).catch(function(err){
    console.error(err);
})
                    

Making promises

bad


fetchSomething = function(){
    var url = createPath()    //what if it throws?
    return promiseFetch(url)  //a promise for http req
}
                    

good


fetchSomething = function(){
   // just to start with
   return Promise.resolve("whatever").then(function(){
       //now an error thrown here will propagate
        return createPath()
    }).then(function(url){
        return promiseFetch(url)
    })
}
                    

Making promises 2

Do not use `.defer()` available in some implementations


fetchSomething = new Promise(function(resolve, reject){
    resolve("it's ok")
    //in case of failure
    reject(Error("it went bad"))
})
                    

Timing


var guessWho = "";
Promise.resolve("resolvedPromise").then(function(){
    guessWho = "in"
})
guessWho = "out"
                    

The final value is "in" because then callback is always asynchronous, even if the promise is already resolved when .then is called. No stackoverflows if you make an infinite loop

Simultaneous stuff

Let's add two numbers that you have to fetch separately in unknown order


function add(getX,getY,cb) {
    var x, y;
    getX( function(xVal){
        x = xVal;
        // both are ready?
        if (y != undefined) {
            cb( x + y );    // send along sum
        }
    } );
    getY( function(yVal){
        y = yVal;
        // both are ready?
        if (x != undefined) {
            cb( x + y );    // send along sum
        }
    } );
}
                    

Isn't that painful

Simultaneous stuff


Promise.all([fetchX(),fetchY()]).then(function(results){
    return results[0]+results[1]
})
                    

or


Promise.all([fetchX(),fetchY()]).spread(function(x,y){
    return x+y
})
                    

Advanced

Feel free to stop understanding at any point

Chaining a lot


var queue = Promise.resolve(initialValue)
for(var i=0; i<todos.length; i++){
    queue = queue.then(todos[i])
}
                    

Still a little ugly, let's try again


var queue = todos.reduce(function(qu,todo){
    return qu.then(todo)
},Promise.resolve(initialValue))
                    
There's a oneliner for showoffs in Q's docs

Conditional error recovery


fetchSomeList().catch(function(err){
    if (err.statusCode==404){
        return []
    } else {
       //rethrow all other errors
        throw err
    }
}).then(function(list){
    return use(list)
}).catch(...)
                    

Adapters

If you need to use a standard node-style asymchronous function, no need to manually wrap it with promise constructors. All popular promise implementations have helpers for that


var readFile = Bluebird.promisify(require("fs").readFile);
                    

And there's a converter, for a cleaner look


var readFile = Q.denodeify(require("fs").readFile);
                    

Retrying

Reminder: a promise cannot be resolved multiple times. If someone wants it to, please call them an idiot.

But what if I need to retry an asynchronous action in a chain?

Take this as a reference for your implementation:
https://github.com/SkPhilipp/Retry

Worth mentioning - every retry is a new promise for an identical action. But if one failed, it cannot be repurposed.

Traps:

  • Avoid deferreds and promise constructors if possible - one thing returning a promise is enough, you can even start a chain with Promise.resolve()
  • Always put .catch() at the end of a chain
  • jQuery has a broken implementation of promises. Really.
    Don't use them, wrap: Q.when($.get(...)) or Promise.resolve($.get(...))
    [mentioned here]

Recommended reading:

Promise me you'll use it

@naugtur
http://naugtur.pl