ES6 Promises: Patterns and Anti-Patterns

Bobby Brennan
Sep 25, 2017 · 7 min read

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: and . For instance, say we have an API client with three methods, , , and , each of which returns a Promise:

Each call to creates another step in the Promise chain, and if there’s an error at any point in the chain, the next block will be triggered. Both and can either return a raw value or a new Promise, and the result will be passed to the next 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 into a Promise-driven function:

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

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

Promisifying values

ES6 has two convenient helper functions for creating Promises out of ordinary values: and . 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 , it’s good practice to always pass in an .

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!

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 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 , 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 in ES6 (why?), but can help us:

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

Racing

Another convenient helper function provided by ES6 (though one I honestly haven’t used much) is . Like , 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 block, which will catch an error in any of the preceding blocks:

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

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

Throwing Errors

You should consider all the code inside your statements as being inside of a block. Both and will cause the next block to run.

This means runtime errors will also trigger your blocks, so don’t make assumptions about the source of your error. For instance, in the following code, we might expect the block to only get errors thrown by , but as the example shows, it also catches the runtime error inside our 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 by writing ; 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 s.

Failure to return

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

Because we didn’t put a in front of on line 4, that particular block resolves immediately, and will probably be called before finishes.

In my opinion, this is a major issue with ES6 promises, and often leads to unexpected behavior. The problem is that can return either a value or a new Promise, and 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 returned , but for now we just have to be careful to any Promises we create.

Calling multiple times

According to the spec, it’s perfectly valid to call 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 each time we call , we never see the returned. But because we update each time we call , 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 , we still reach the second .

There are probably valid reasons to call 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 or — 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 block! This is because is called inside both and , making it part of the Promise chain.

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

Not catching errors

Error handling in JavaScript is a strange beast. While it supports the familiar 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 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 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

DataFire.io

Blog for datafire.io