Patterns of Promise Use

Forbes Lindesay

slides.forbesl.co.uk

What is a callback?

function readJSON(filename, callback) {
  fs.readFile(filename, 'utf8', function (err, res) {
    if (err) return callback(err);
    callback(null, JSON.parse(res));
  });
}

What is a callback?

function readJSON(filename, callback) {
  fs.readFile(filename, 'utf8', function (err, res) {
    if (err) return callback(err);
    try {
      res = JSON.parse(res);
    } catch (ex) {
      return callback(ex);
    }
    callback(null, res);
  });
}

What is a promise?

  • The result of an asynchronous operation
  • It can be in one of three states
    • Pending
    • Fulfilled
    • Rejected

Creating a Promise

function readFile(filename, enc) {
  return new Promise(function (fulfill, reject) {
    fs.readFile(filename, enc, function (err, res) {
      if (err) reject(err);
      else fulfill(res);
    });
  });
}

Getting the value of a Promise

function readJSON(filename) {
  return new Promise(function (fulfill, reject) {
    readFile(filename, 'utf8').done(function (res) {
      try {
        fulfill(JSON.parse(res));
      } catch (ex) {
        reject(ex);
      }
    }, reject);
  });
}

Chaining asynchronous operations

function readJSON(filename) {
  return readFile(filename, 'utf8').then(function (res) {
    return JSON.parse(res);
  });
}

readJSON('file.json').done(function (obj) {
  console.log(obj);
}, function (err) {
  console.log('Error reading file:');
  throw err;
});

What did we gain?

  • Avoided conflating the input with the output
  • Composability with less chance of race conditions
  • Implicit error handling

Async.js - Waterfall

Waterfall

function checkAccess(userID, resource) {
  var user = getUser(userID);
  var userPermissions = user.getPermissions();
  return userPermissions.checkAccess(resource);
}

Waterfall

function checkAccess(userID, resource, callback) {
  getUser(userID, function (err, user) {
    if (err) return callback(err);
    user.getPermissions(function (err, userPermissions) {
      if (err) return callback(err);
      userPermissions.checkAccess(resource, callback);
    });
  });
}

Waterfall

function checkAccess(userID, resource, callback) {
  async.waterfall([
    function (callback) {
      getUser(userID, callback);
    },
    function (user, callback) {
      user.getPermissions(callback);
    },
    function (userPermissions, callback) {
      userPermissions.checkAccess(resource, callback);
    }
  ], callback);
}

Waterfall

function checkAccess(userID, resource) {
  return getUser(userID).then(function (user) {
    return user.getPermissions();
  }).then(function (userPermissions) {
    return userPermissions.checkAccess(resource);
  });
}

Async.js - Map

Array.prototype.map

var numbers = [1, 2, 3, 4];
var doubled = numbers.map(function double(n) {
  return n * 2;
});
// doubled => [2, 4, 6, 8]

Async.map

var numbers = [1, 2, 3, 4];
async.map(numbers, function slowDouble(n, callback) {
  setTimeout(function () {
    callback(null, n * 2);
  }, 10000);
}, function (err, doubled) {
  // doubled => [2, 4, 6, 8]
});

Map Promises

function slowDouble(n) {
  return new Promise(function (fulfill) {
    setTimeout(function () {
      fulfill(n * 2);
    }, 10000);
  });
}

Map Promises

var numbers = [1, 2, 3, 4];
var promises = numbers.map(slowDouble);
// promises => [Promise(2), Promise(4), Promise(6), Promise(8)]

Map Promises

var numbers = [1, 2, 3, 4];
var promises = numbers.map(slowDouble);
// promises => [Promise(2), Promise(4), Promise(6), Promise(8)]
var doubled = promises.reduce(function (accumulator, promise) {
  return accumulator.then(function (accumulator) {
    return promise.then(function (value) {
      accumulator.push(value);
      return accumulator;
    });
}, Promise.from([]));
// doubled => Promise([2, 4, 6, 8])

Promise.all

Promise.all = function (promises) {
  return promises.reduce(function (accumulator, promise) {
    return accumulator.then(function (accumulator) {
      return promise.then(function (value) {
        accumulator.push(value);
        return accumulator;
      });
  }, Promise.from([]));
};

Map using Promise.all

var numbers = [1, 2, 3, 4];
var doubled = Promise.all(numbers.map(slowDouble));
// doubled => Promise([2, 4, 6, 8])

Throttling

mapSerial

var numbers = [1, 2, 3, 4];
async.mapSerial(numbers, function (n, callback) {
  setTimeout(function () {
    callback(null, n * 2);
  }, 10000);
}, function (err, doubled) {
  // doubled => [2, 4, 6, 8]
});

One at a Time

function oneAtATime(fn) {
  var ready = Promise.from(null);
  return function () {
    var args = arguments;
    var result = ready.then(function () {
      return fn.apply(null, args);
    });
    ready = result.then(null, function () {});
    return result;
  };
}

mapSerial

var numbers = [1, 2, 3, 4];
var doubled = Promise.all(numbers.map(oneAtATime(slowDouble)));

mapLimit

var numbers = [1, 2, 3, 4];
async.mapLimit(numbers, 2, function (n, callback) {
  setTimeout(function () {
    callback(null, n * 2);
  }, 10000);
}, function (err, doubled) {
  // doubled => [2, 4, 6, 8]
});

Limit

function throttle(limit, fn) {
  var queue = [];
  function run(self, args) {
    if (limit) {
      limit--;
      var result = fn.apply(self, args);
      result.done(release, release);
      return result;
    } else {
      return new Promise(function (fulfill) {
        queue.push({fulfill: fulfill, self: self, args: args})
      })
    }
  }
  function release() {
    limit++;
    if (queue.length) {
      var next = queue.shift();
      next.fulfill(run(next.self, next.args));
    }
  }
  return function () {
    return run(this, arguments);
  }
}

Throat

var throttle = require('throat');
var throttled = throttle(4, fn);
//now throttled calls fn only 4 times in parallel
for (var i = 0; i < 1000; i++) {
  throttled(i);
}

mapSerial / mapLimit

var throat = require('throat');
var numbers = [1, 2, 3, 4];
var doubledSer = Promise.all(numbers.map(throat(1, slowDouble)));
var doubledLim = Promise.all(numbers.map(throat(2, slowDouble)));

Async.js - Filter

Array.prototype.filter

var files = ['foo.txt', 'bar.txt'];
var existing = files.filter(function (n) {
  return fs.existsSync(n);
});
// existing => ['foo.txt']

Async.filter

var files = ['foo.txt', 'bar.txt'];
async.filter(files, fs.exists, function (existing) {
  // existing => ['foo.txt']
});

Async.filter

var files = ['foo.txt', 'bar.txt'];
async.filter(files, function (filename, callback) {
  fs.exists(filename, function (exists) {
    callback(exists);
  });
}, function (existing) {
  // existing => ['foo.txt']
});

Async.filter

var entries = ['foo.txt', 'bar'];
async.filter(entries, function (name, callback) {
  fs.stat(name, function (err, stat) {
    if (err) throw err; // WTF!
    callback(stat.isDirectory());
  });
}, function (directories) {
  // directories => ['bar']
});

Promise Filter

function filter(array, fn) {
  return Promise.all(array.map(fn)).then(function (filtered) {
    return array.filter(function (item, index) {
      return filtered[index];
    });
  });
}

Promise Filter

var files = ['foo.txt', 'bar.txt'];
var existing = filter(files, existsPromise);
// existing => Promise(['foo.txt'])

var entries = ['foo.txt', 'bar'];
var directories = filter(files, function (name) {
  return statPromise(name).then(function (stat) {
    return stat.isDirectory();
  });
});
// directories => Promise(['bar'])

Races

Promise.race

Promise.race = function (array) {
  return new Promise(function (fulfill, reject) {
    array.forEach(function (promise) {
      promise.done(fulfill, reject);
    });
  });
};

Some

function some(array, fn) {
  return new Promise(function (, reject) {
    var all = array.map(fn).map(function (promise) {
      return promise.then(function (matches) {
        if (matches) fulfill(true);
      });
    });
    Promise.all(all).done(function () {
      fulfill(false);
    }, reject);
  });
}

Real World Use

Express

var express = require('express');
var app = express();

app.use(function (req, res, next) {
  database.getUser(req.userId).then(function (user) {
    req.user = user;
  }).nodeify(next);
});

app.get('/:item', function (req, res, next) {
  database.getItem(req.params.item).then(function (item) {
    res.render('item-view', {item: item});
  }).done(null, next);
});

app.listen(3000);

Mocha

describe('my feature', function () {
  it('rocks', function (done) {
    doAwesomeAsync().then(function (res) {
      assert(res, 'res should be awesome');
    }).nodeify(done);
  });
});

Mocha As Promised

require('mocha-as-promised');
describe('my feature', function () {
  it('rocks', function () {
    return doAwesomeAsync().then(function (res) {
      assert(res, 'res should be awesome');
    });
  });
});

Errors

Consistent

Composable

Forbes Lindesay

slides.forbesl.co.uk

Social Networks

Twitter: @ForbesLindesay

GitHub: @ForbesLindesay

Blog: www.forbeslindesay.co.uk

Open Source

Jade

Browserify Middleware

readable-email.org

brcdn.org

tempjs.org