Video

Want to see the full-length video right now for free?

Notes

What are promises?

Promises are an abstraction that makes working with asynchronous code more manageable and consistent. Rather than passing a callback function to an asynchronous operation (and possibly falling into the dreaded callback pyramid of doom), the asynchronous operation can return a "Promise" representing the future value, or failure.

Callback versus Promises

Here's an example in Node for finding the largest file in a directory (taken from strongloop):

var fs = require('fs')
var path = require('path')

module.exports = function (dir, cb) {
  fs.readdir(dir, function (er, files) {
    if (er) return cb(er)
    var counter = files.length
    var errored = false
    var stats = []

    files.forEach(function (file, index) {
      fs.stat(path.join(dir,file), function (er, stat) {
        if (errored) return
        if (er) {
          errored = true
          return cb(er)
        }
        stats[index] = stat

        if (--counter == 0) {
          var largest = stats
            .filter(function (stat) { return stat.isFile() })
            .reduce(function (prev, next) {
              if (prev.size > next.size) return prev
              return next
            })
          cb(null, files[stats.indexOf(largest)])
        }
      })
    })
  })
}

...and here's that same code, using the [Q promises library][q]:

[q]: http://documentup.com/kriskowal/q/

var fs = require('fs')
var path = require('path')
var Q = require('q')
var fs_readdir = Q.denodeify(fs.readdir)
var fs_stat = Q.denodeify(fs.stat)

module.exports = function (dir) {
  return fs_readdir(dir)
    .then(function (files) {
      var promises = files.map(function (file) {
        return fs_stat(path.join(dir,file))
      })
      return Q.all(promises).then(function (stats) {
        return [files, stats]
      })
    })
    .then(function (data) {
      var files = data[0]
      var stats = data[1]
      var largest = stats
        .filter(function (stat) { return stat.isFile() })
        .reduce(function (prev, next) {
        if (prev.size > next.size) return prev
          return next
        })
      return files[stats.indexOf(largest)]
    })
    .catch(console.log(error));
}

Let's go through all of that:

We're using Q.denodeify to create Q-compatible functions from Node functions. The then function takes a function to execute once the promise is finished. We can chain .then (like Q(function).then(...).then(...)) to run a bunch of promises one after each other. The all function takes an array of promises and wraps it in a promise that resolves once all the promises have resolved.

The core idea of promises is that rather than passing a function that represents the next step (a "callback"), the promise inverts control and returns an object that represents the future state where we've resolved the asynchronous action and allows our code to maintain control over the flow of execution.

Promise A+ specification

[The A+ spec][a+] is the specification for how promises must behave. Promises start in the pending state and can move to resolved or rejected. The then method takes two arguments, onFulfilled and onRejected. onFulfilled takes the value of the successful computation, and onRejected takes the reason for rejection. then can be called on the same promise multiple times and their functions will run in the order they were called.

[a+]: https://promisesaplus.com/

Creating promises

This code creates a promise that waits 3 seconds then transitions to the resolved state with the value 42:

// A Promise that returns a value
function theUltimateAnswer() {
  return new Promise(function(resolve) {
    setTimeout(function(){
      var value = 42;
      resolve(value);
    }, 3000);
  });
}

promise = theUltimateAnswer()
// Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}

promise
// Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}

promise
// Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 42}

This code creates a promise that waits 3 seconds, then rejects:

// A Promise that just fails
function waitForIt() {
  return new Promise(function(resolve, reject) {
    setTimeout(function(){
      reject("Oh noes!");
    }, 3000);
  });
}

promise = waitForIt()
// Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}

promise
// Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}

// Uncaught (in promise) Oh noes!(anonymous function) @ VM194:6setTimeout

promise
// Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: "Oh noes!"}

This code sample shows how to use the value from a successful promise:

function howDoIGetThatValue() {
  return new Promise(function(resolve) {
    setTimeout(function(){
      var value = 42;
      resolve(value);
    }, 1000);
  });
}

howDoIGetThatValue().then(function(result) {
  console.log(result);
});

This code sample shows how to handle a rejected promise:

// A Promise that just fails
function waitForIt() {
  return new Promise(function(resolve, reject) {
    setTimeout(function(){
      reject("Oh noes!");
    }, 1000);
  });
}

promiseWithThen = waitForIt().then(null, function(error){
  console.log(error);
});

promiseWithCatch = waitForIt().catch(function(error){
  console.log(error);
});

Errors will propagate along the chain:

// Where does it fail?
function waitForIt() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      if (Math.random() < 0.5) {
        reject("Oh noes!");
      } else {
        resolve("Everything is fine.");
      }
    }, 1000);
  });
}

waitForIt().then(function(result) {
  throw("Just kidding!");
}).catch(function(error) {
  console.log(error);
});

Code examples

The following code is taken from [electron-boilerplate]:

[electron-boilerplate]: https://github.com/szwacz/electron-boilerplate/blob/4e1333bb2e79fd18476742e7f658a2519e5fa7bc/tasks/release_osx.js#L136-L147

module.exports = function () {
  return init()
    .then(copyRuntime)
    .then(cleanupRuntime)
    .then(packageBuiltApp)
    .then(finalize)
    .then(renameApp)
    .then(createInstaller)
    .then(cleanClutter)
    .catch(console.error);
};
  • Removing loading class from a modal.
request.then(function () {
  modalForm.form.removeClass('loading');
});
  • An example from an ember application showing.
return this.get('favoritedItems').then(items => {
  return Ember.RSVP.Promise.all(
    items.mapBy('topics')
  ).then(topics => {
    return Ember.RSVP.Promise.all(
      topics.mapBy('items')
    );
  });
});

Further reading

  • [The Promises A+ spec][a+] - The core specification, defining exactly what Promises are and aren't.
  • [Promises in Wicked Detail][] - A wonderful post where the author builds a Promise implementation from first principles.
  • [Mozilla Developer Network Promise documentation][] - MDN is the go to resource for things web, and Promises are no different.
  • [Using JavaScript Promises to Reason About User Interaction][] - Blog post on Giant Robots.
  • [You're Missing the Point of Promises][] - Article that highlights the importance of the fact that promises allow us to always return a consistent value (a Promise!).
  • [Promise Anti-patterns][] - A great way to dive deep on Promises, learning by counter example.

[Promises in Wicked Detail]: http://www.mattgreer.org/articles/promises-in-wicked-detail/ [Mozilla Developer Network Promise documentation]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise [Using JavaScript Promises to Reason About User Interaction]: https://robots.thoughtbot.com/using-javascript-promises-to-reason-about-user-interaction [You're Missing the Point of Promises]: https://blog.domenic.me/youre-missing-the-point-of-promises/ [Promise Anti-patterns]: http://taoofcode.net/promise-anti-patterns/