ES6 Promises: Patterns and Anti-Patterns

When I first got started with NodeJS a few years ago, I was mortified by what is now affectionately known as “callback hell”. Fortunately, here in 2017, NodeJS has adopted the latest and greatest JavaScript features, and since v4 has supported Promises.

But while Promises make code more concise and readable, they can be a bit daunting to those who are only familiar with callbacks. Here I’ll lay out a few basic patterns I’ve learned while working with Promises, as well as some gotchas.

Note: I’ll be using arrow functions throughout this article — if you’re not familiar, they’re pretty straightforward, but I’d suggest reading up on their benefits

Patterns and Best Practices

Using Promises

If you’re using a third-party library that already supports Promises, usage is pretty straightforward. There are only two functions you need to worry about: then() and catch(). For instance, say we have an API client with three methods, getItem(), updateItem(), and deleteItem(), each of which returns a Promise:

Each call to then() creates another step in the Promise chain, and if there’s an error at any point in the chain, the next catch() block will be triggered. Both then() and catch() can either return a raw value or a new Promise, and the result will be passed to the next then() in the chain.

For comparison, here’s the equivalent logic using callbacks:

The first difference to notice is that with callbacks, we have to include error handling at every step in the process, rather than having a single catch-all error block. The second issue with callbacks is more stylistic — each step gets indented horizontally, obscuring the seriality that’s obvious when looking at the Promise-driven code.

Promisifying callbacks

One of the first tricks you’ll need to learn is how to convert callbacks to Promises. You might be working with a library that still uses callbacks, or with your own legacy code, but fortunately it only takes a few lines to wrap it with a Promise. Here’s an example that converts Node’s callback-driven fs.readFile into a Promise-driven function:

The critical piece is the Promise constructor — it takes in a function, which in turn has two function parameters: resolve and reject. Inside this function is where we do all our work, and when we’re done we call resolve if successful, and reject if there’s an error.

Note that only one of resolve or reject should be called, and it should be called only once. In our example, if fs.readFile returns an error, we pass the error to reject; otherwise we pass the file data to resolve.

Promisifying values

ES6 has two convenient helper functions for creating Promises out of ordinary values: Promise.resolve() and Promise.reject(). For instance, you might have a function that needs to return a Promise, but handles certain cases synchronously:

Note that while you can pass anything (or nothing) to Promise.reject(), it’s good practice to always pass in an Error.

Running concurrently

Note: a previous version incorrectly used the word “parallel” rather than “concurrent” — see the comments for a great explanation of the difference. Thanks Carlos!

Promise.all is a convenient method for running an array of Promises concurrently, i.e. all at the same time. For instance, say we have a list of files we want to read from disk. Using the readFilePromise function we created above, it would look like this:

I won’t even try and write the equivalent code using traditional callbacks. Suffice it to say, it would be messy and bug-prone.

Running in series

Sometimes running a bunch of Promises at the same time can cause issues. For instance, if you try to retrieve a bunch of resources from an API using Promise.all, it may start responding with 429 errors as you hit your rate limit.

One solution is to run the Promises in series, or one after the other. Unfortunately there’s no simple analog to Promise.all in ES6 (why?), but Array.reduce can help us:

In this case, we wait for each call to api.deleteItem() to finish before making the next call. This is just a more concise/generic way of writing a .then() for each itemID:

Racing

Another convenient helper function provided by ES6 (though one I honestly haven’t used much) is Promise.race. Like Promise.all, this takes in an array of Promises and runs them concurrently, but it will return once any of the Promises resolves or rejects, discarding all the other results.

For instance, we can create a Promise that times out after a certain number of seconds:

Note that the other Promises will continue to run — you just won’t ever see the results.

Catching errors

The most common way to catch errors is to add a .catch() block, which will catch an error in any of the preceding .then() blocks:

Here the catch() block is triggered if either getItem or updateItem fails. But what if we want to handle the getItem error separately? Just insert another catch() higher up — it can even return another Promise.

Now, if getItem() fails, we intervene and create a new item.

Throwing Errors

You should consider all the code inside your then() statements as being inside of a try block. Both return Promise.reject() and throw new Error() will cause the next catch() block to run.

This means runtime errors will also trigger your catch() blocks, so don’t make assumptions about the source of your error. For instance, in the following code, we might expect the catch() block to only get errors thrown by getItem, but as the example shows, it also catches the runtime error inside our then() statement.

Dynamic chains

Sometimes we want to construct our Promise chain dynamically, e.g. inserting an extra step if a particular condition is met. In the example below, before reading a given file we optionally create a lock file:

Be sure to update the value of promise by writing promise = promise.then(/*...*/); see the anti-pattern Calling then() multiple times below.

Anti-patterns

Promises are a neat abstraction, but it’s easy to fall into certain traps. Below are a few of the most frequent problems I encounter.

Recreating callback hell

When first moving from callbacks to Promises, I found it hard to shed some old habits, and found myself nesting Promises just like callbacks:

This kind of nesting is almost never necessary. Sometimes one or two levels of nesting can help to group related tasks, but you can almost always rewrite Promise nests as a vertical chain of .then()s.

Failure to return

A frequent and pernicious error I run into is forgetting a return statement inside a Promise chain. Can you spot the bug below?

Because we didn’t put a return in front of api.updateItem() on line 4, that particular then() block resolves immediately, and api.deleteItem() will probably be called before api.updateItem() finishes.

In my opinion, this is a major issue with ES6 promises, and often leads to unexpected behavior. The problem is that .then() can return either a value or a new Promise, and undefined is a perfectly valid value to return. Personally, if I’d been in charge of the Promise API, I’d have thrown a runtime error if .then() returned undefined, but for now we just have to be careful to return any Promises we create.

Calling .then() multiple times

According to the spec, it’s perfectly valid to call then() multiple times on the same Promise, and the callbacks will be invoked in the same order they’re registered. However, I’ve never seen a valid reason for doing this, and it can create subtly unexpected behavior when using return values and error handling:

In this example, because we don’t update the value of p each time we call then(), we never see the 'b' returned. But because we update q each time we call then(), its behavior is more predictable.

This also applies to error handling:

Here we expect throwing an Error to break the promise chain, but because we don’t update the value of p, we still reach the second then().

There are probably valid reasons to call .then() multiple times on a single Promise, as it allows you to effectively fork the Promise into several new, independent Promises, but I’ve never found a real-world use case.

Mixing callbacks and Promises

This is an easy trap to fall in if you’re using a Promise-based library, but still working in a callback-based project. Always avoid calling a callback from inside then() or catch() — otherwise the Promise will swallow any subsequent errors as part of the Promise chain. For instance, the following seems like a reasonable way to wrap a Promise with a callback:

The problem here is that if there is an error, we’ll get a warning about an “Unhandled promise rejection”, even though we’ve added a catch() block! This is because callback() is called inside both then() and catch(), making it part of the Promise chain.

If you absolutely must wrap a Promise with a callback, you can use setTimeout (or in NodeJS, process.nextTick) to break out of the Promise:

Not catching errors

Error handling in JavaScript is a strange beast. While it supports the familiar try/catch paradigm, there’s no way to force callers to handle errors the way e.g. Java does. However, with callbacks, it became common to use so-called “errbacks” — callbacks whose first parameter is an error. This forces callers to at least acknowledge the possibility of an error. For example, with the fs library:

With Promises, it’s once again easy to forget that errors need to be explicitly handled, especially for sensitive operations like filesystem and database access. Currently if you fail to catch a rejected Promise, you’ll see a very ugly warning in NodeJS:

(node:29916) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: whoops!
(node:29916) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Be sure to add a catch() to the end of any Promise chain in your main event loop to avoid this.

Wrapping Up

I hope this has been a helpful overview of common Promise patterns and anti-patterns. If you want to learn more, here are a few helpful resources:

Mozilla docs for ES6 Promises

An intro to Promises from Google

An overview of ES6 Promises by Dave Atchley

Even more Promise patterns and anti-patterns

Or read more from the DataFire team