Promises vs Callbacks

FDS Web Dev Club

Bryant Rolfe

Rules of the Road

  • Intermediate - Advanced level presentation
  • Familiarity with NodeJS will help
  • Read the code snippets!
  • Expect some brain pain
  • It'll be worth it

Game Plan

  1. Why do I care what promises are?
  2. Fine, what are they?
  3. Sweet! Show me how they work!
  4. OMG! Mind == Shattered
  5. There's more?! Deep breaths...
  6. Recovery Time (Q&A)

Asynchronous Programming --> Callbacks

Call my function when you're done doing your async thing, please.


var myAsyncFunction = function(foo, bar, baz, callback){
    setTimeout(function(){
        var result = foo + bar + baz;
        callback(null, result);
    }, 500);
};
// callback signature: callback(err, results)
            

fs.readFile('~/.profile-user',
    // some time passes...
    function(err, contents){
        // now we have the contents!
    }
);
            

Let's see a more realistic example...


var async = require('async'),
    fs    = require('fs');

var paths = ['file1.txt', 'file2.txt', 'file3.txt'];

async.map(paths, fs.stat, function(error, results) {
  // use the stat results
});
            

Not bad, right?

Until we want to re-use a piece of that information...


var paths = ['file1.txt', 'file2.txt', 'file3.txt'];

async.map(paths, fs.stat, function(error, results) {
  // use the results
});

fs.stat(paths[0], function(error, stat) {
  // use stat.size
});
            

But now we are stat-ing the file twice!

Fine, we'll just handle the first one specially...


var paths = ['file1.txt', 'file2.txt', 'file3.txt'];

async.map(paths, fs.stat, function(error, results) {
  var size = results[0].size;
  // use size
  // use the results
})
            

Now our size task is blocking on all those other files!

Ok. Then we'll just handle that file on its own...


var paths = ['file1.txt', 'file2.txt', 'file3.txt'],
    file1 = paths.shift();

fs.stat(file1, function(error, stat) {
  // use stat.size
  async.map(paths, fs.stat, function(error, results) {
    results.unshift(stat);
    // use the results
  });
});
            

Now all the other files block on the first one. Sad face.

Last try!


var paths = ['file1.txt', 'file2.txt', 'file3.txt'],
    file1 = paths.shift();

async.parallel([
  function(callback) {
    fs.stat(file1, function(error, stat) {
      // use stat.size
      callback(error, stat);
    });
  },
  function(callback) {
    async.map(paths, fs.stat, callback);
  }
], function(error, results) {
  var stats = [results[0]].concat(results[1]);
  // use the stats
});           
            

Victory!

That wasn't terribly elegant...

We can do better.

Promises, baby

A promise is...

  • An object!
  • it has methods
  • and properties

Promise API

  • promise.then(success, error, progress)
  • promise.done(callback)
  • promise.fail(callback)

These methods return new promises

Note: There are many promise libraries out there. Some are better than others. See this blog post for more info.

A toy example...


var promise = fsStat('~/.profile-user');
promise.then(function(contents){
    // We have the contents!
});
            

Got it?

Let's revist our original example...


var fs = require('fs'),
    Q  = require('q');

var fsStat = Q.nfbind(fs.stat); // Create a promise version of fs.stat
            

var paths = ['file1.txt', 'file2.txt', 'file3.txt'];

var statsPromises = paths.map(fsStat);

// We now have promise objects for all the stat operations
            

// When all of the promises are resolved, use their results
Q.all(statsPromises).then(function(results){
    // use the results  
});
            

What about the size part?


statsPromises[0].then(function(stat){
    // use the size
});
            

All together now!


var paths = ['file1.txt', 'file2.txt', 'file3.txt'];

var statsPromises = paths.map(fsStat);

// We now have promise objects for all the stat operations

// When all of the promises are resolved, use their results
Q.all(statsPromises).then(function(results){
    // use the results  
});

statsPromises[0].then(function(stat){
    // use the size
});
            

Ahhh...that's better

What did we just do?

  • Hard --> Easy
  • Avoided "callback hell"
  • Set up relationships.
  • Let the computer handle control flow.

Halftime!

Where are we?

  1. Why do I care what promises are?
  2. Fine, what are they?
  3. Sweet! Show me how they work!
  4. OMG! Mind == Shattered
  5. There's more?! Deep breaths...
  6. Recovery Time (Q&A)

Chaining Promises

Remember that promise.then() returns a new promise:


var anotherPromise = promise.then(function(){});
            

When anotherPromise resolves depends on what the handler for then returns.

Can return simple data...


fsStat('file1.txt')
.then(function(stat){
    // called when fsStat completes
    var transformedData;
    // transform the data...
    return transformedData;
})
.then(function(transformedData){
    // called after previous handler returns with its results
    console.log(transformedData);
});
            

The chained promise resolves immediately after handler returns.

Can return promises...


fsExists('file1.txt')
.then(function(exists) {
    if(exists) {
        return fsStat('file1.txt');
    } else {
        return fsWriteFile('file1.txt', "some data").then(function() {
           return fsStat('file1.txt') 
        });
    }
})
.then(function(stat) {
    // called only after all chained promises prior to this one resolve
    console.log(stat);
});
            

Chained promise resolves after promises returned by previous handlers resolve.

Can aggregate promises...


var p1 = fsStat('file1.txt');
var p2 = fsStat('file2.txt');

Q.all(p1, p2).then(function(results){
    var file1Stats = results.shift();
    var file2Stats = results.shift();

    // do something with both stats
})
            

Really useful pattern for rendering a view that might depend on two separate ajax fetches.

Can execute async tasks sequentially...


var paths = ['file1.txt', 'file2.txt', 'file3.txt'],
var allDone = paths.reduce(function(promise, currentPath){
    return soFar.then(function(){
        return fsUnlink(currentPath);
    });
}, Q());

allDone.then(function() {
    // all files have been unlinked
})
// Q() returns a pre-resolved promise to start the chain
            

We can all agree...

that promises are sweet.

There's a whole lot more to learn.

Things we skipped:

  • Error handling
  • Deferreds

Recovery Time (Q&A)

Bibliography