Menu

ES6 Promises: Best Practices

After using more and more promises lately, I've gathered a handful of good and bad design patterns for promises. I'm going to list out some examples of how promises are typically used and demonstrate anti-patterns and how to clean them up. Like everyone, I've been guilty to some of these anti-patterns, but it's all good. We're here to learn. I promise I'll try not to use any puns. Ah crap.

Callback Style

You may be tempted to attach both fulfillment and rejection handlers in a .then() call. For example:

somethingAsync().then(function(result) {  
  // Do something on success/fulfillment
}, function(err) {
  // Do something on error/rejection
});

The problem with that approach is that what if an error is thrown within your success handler? Your error handler won't catch that, as the error handler will only handle errors thrown from the somethingAsync() call.

Promises have the ability to handle errors or rejections anywhere in the promise chain/composition. As a result, you should use .catch() to handle any errors in the chain. This comes in handy when you have a chain of then's. Example:

somethingAsync()  
  .then(function(result) {
    // Do something on success/fulfillment of somethingAsync()
  })
  .then(function(result) {
    // Do something on success/fulfillment of the previous then()
  })
  .catch(function(err) {
    // Do something on any error/rejection in the chain
  });

Chaining Promises

As we saw in the previous example, promises can be chained. The chain can consist of synchronous or asynchronous operations. Example:

somethingAsync1()  
  .then(function(result) {
    // Handle fulfillment result from somethingAsync1()
    somethingAsync2(result);
  })
  .then(function(result) {
    // Handle fulfillment result from somethingAsync2()
    somethingSync1(result);
  })
  .catch(function(err) {
    // Do something on any error/rejection in the chain
  });

In that example, you may think the order of operations is the following: somethingAsync1() -> somethingAsync2() -> somethingSync1(). However, you don't know how long somethingAsync2() will take, and before it resolves, somethingSync1() would have already been invoked.

When you are chaining promises, make sure you return a value from your .then() calls. This is especially important when you have one then calling an asynchronous operation. If the next then in the chain needs the results from this asynchronous operation, the subsequent then must wait for the operation to be resolved. You should return one of the following values from your then handlers:

  • a new Promise
  • a synchronous value or undefined
  • or, throw an Error or Exception

The previous example should be re-written like so, assuming somethingAsync2() returns a Promise:

somethingAsync1()  
  .then(function(result) {
    // Handle fulfillment result from somethingAsync1()
    return somethingAsync2(result);
  })
  .then(function(result) {
    // Handle fulfillment result from somethingAsync2()
    somethingSync1(result);
  })
  .catch(function(err) {
    // Do something on any error/rejection in the chain
  });

Nested Promises

You've probably seen callback hell. Example:

asyncFunction1(function(err, result) {  
  asyncFunction2(function(err, result) {
    asyncFunction3(function(err, result) {
      asyncFunction4(function(err, result) {
       asyncFunction5(function(err, result) {
         // Do something
       });
      });
    });
  });
});

Xzibit loves callbacks

That can get ugly and out of hand. One way to fix this is to chain promises like we saw earlier. Doing so would give you a flat structure and would allow you to run tasks in a series. Another way is to use Promise.all().

Promise.all() takes an iterable object like an array and returns a Promise. The array should contain promises. If an element isn't a Promise, it is automatically converted to one with Promise.resolve(). All the promises in the array will be executed in parallel, and after they are all resolved, the then handler for the all Promise will handle the results. If any of the promises reject, the all Promise will immediately be rejected. Note that the following example does not depend on the order of which the promises are resolved.

var a = promise1;  
var b = promise2;  
var c = promise3;

Promise.all([a,b,c])  
  .then(function(results) {
    // results[0] contains the result from promise1, results[1] is for promise2, ...
  })
  .catch(function(err) {
    // Handle rejections from any of the promises
  });

Summary

I hope you take these recommendations into consideration when working with promises. Feel free to leave comments on other best practices or tips when working with promises.