Broken Promises

December 29, 2016

About

This post is about how to use promises effectively. In it, I'm going to use some ES6 syntax. ES6 is the latest version of JavaScript. When you see:

// Arrow function
x => x + 1

That's basically the same as:

// Regular function
function(x) { return x + 1; }

If you're not sure if you can use ES6, check out the ES6 compatibility table and Babel. Otherwise, just manually change the arrow functions like shown above.

Error handling

Promises don't have a very large API, but that doesn't mean they're simple to learn. How error handling works is a bit tricky in promises.

First of all, there's p.then(ok). This is the "happy path" handler, and the one you should be using most of the time.

var p = Promise.resolve(1)
  .then(x => x + 1)
  .then(x => x + 1)
  .then(x => x + 1);

// Prints 4
p.then(x => console.log(x));

If one of those steps fail, then the final console.log there will never happen.

var p = Promise.resolve(1)
  .then(x => x + 1)
  .then(function() {
    throw new Error("something goofed up here");
  })
  .then(x => x + 1);

// Nothing is ever printed
p.then(x => console.log(x));

There's also p.then(ok, fail). This calls fail if p is rejected, but does not call fail if the ok handler is rejected. Also, p.catch(fail) is a shortcut for p.then(null, fail).

var p = Promise.resolve(1)
  .then(x => x + 1)
  .then(function() {
    throw new Error("some error");
  })
  .catch(function() {
    // Prints "There was an error"
    console.error("There was an error");
    return 1;
  })
  .then(x => x + 1);

// Prints 2
p.then(x => console.log(x));

You should always either throw or return from the fail handlers for your promises, because JavaScript will implicitly return undefined at the end of a function for you, which you probably don't want in your promise value:

var p = Promise.resolve(1)
  .then(function() {
    throw new Error("oopsy");
  })
  .catch(function(err) {
    // Prints "I found an error:", and the error
    console.error("I found an error:", err);
  });

// Prints undefined
p.then(x => console.log(x));

So if you just rethrow the error for errors you can't actually recover from, you'll be fine:

var p = Promise.resolve(1)
  .then(function() {
    throw new Error("oopsy");
  })
  .catch(function(err) {
    // Prints "I found an error:", and the error
    console.error("I found an error:", err);
    throw err;
  });

// Doesn't print anything
p.then(x => console.log(x));

Value your promises

The most important thing about promises, compared to callbacks, is that promises are values. That means you can assign them to a variable, return them from a function, or pass them to another function. If you don't do something with the value of your promise, you're losing information! You might not always need this information, but it's there, and don't forget it.

So instead of something like this:

var theData = {};

function onLoad() {
  console.log("Hello, " + theData.name);
}

function init() {
  request("/my-data").then(function(data) {
    theData = data;
    onLoad();
  });
}

init();

You can write this:

function getData() {
  return request("/my-data");
}

function onLoad(data) {
  console.log("Hello, " + data.name);
}

getData().then(onLoad);

Pyramids of doom

Often you'll see .then chains nested, forming this "pyramid" shape:

function chaseData(url) {
  return request(url).then(res1 =>
    request(res1.someUrl).then(res2 =>
      request(res2.anotherUrl).then(res3 =>
        res3.data
      )
    )
  );
}

This often happens because the value from one promise is used to get another promise, and so on. But promises were built for handling this already! If you have a simple pipeline where the next operation only depends on the previous, you can totally flatten your pyramid into a nice and easy road:

function chaseData(url) {
  return request(url)
    .then(res => request(res.someUrl))
    .then(res => request(res.anotherUrl))
    .then(res => res.data);
}

Ta-da! Much simpler to understand now. But I'm sure some of you at home are thinking, "but what about cases where you need to keep all the data until the final step". Well, you're right, that still is a little tricky. Sometimes you'll see code like this:

function something(urlA, urlB) {
  return request(urlA).then(a =>
    request(urlB).then(b =>
      [a, b]
    )
  );
}

In this scenario, there's a handy promise function called Promise.all:

function something(urlA, urlB) {
  return Promise.all([
    request(urlA),
    request(urlB)
  ]);
}

Of course, there's also the more complicated case where you need a in order to determine urlB, such as in this example:

function userWithImage(userId) {
  var data = {};
  return request("/users/" + userId)
    .then(function(user) {
      data.user = user;
      return request(user.avatarImageUrl);
    })
    .then(image => [data.user, image]);
}

Just making a temporary data object you can reference in the next step of the pipeline completely flattens the need for nesting.

Don't catch and release

If you're making a new function that returns a promise but you know you might encounter an error, you might write something like this:

function asyncThing(x) {
  return new Promise(function(resolve, reject) {
    try {
      resolve(f(x));
    } catch (error) {
      reject(error);
    }
  });
}

But the Promise constructor (and .then functions) are designed to automatically call reject for you if something is thrown! Which means we can ignore the reject parameter entirely.

function asyncThing(x) {
  return new Promise(function(resolve) {
    try {
      resolve(f(x));
    } catch (error) {
      throw error;
    }
  });
}

And then at this point there's no reason to even have the try/catch any more, we can just simplify to:

function asyncThing(x) {
  return new Promise(function(resolve) {
    resolve(f(x));
  });
}

The future holds promise

Go forth and use promises elegantly! Good luck and have fun with JavaScript! ☕️