JavaScript Promises

The Weekly Iteration

This video is only a short sample, but you can access the full version and all our other great content by subscribing.

Video

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:

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

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:

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

×

15 Full Courses, 100+ Screencasts & New Content Weekly