ES6 Promises: Patterns and Anti-Patterns
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.
Patterns and Best Practices
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:
catch(). For instance, say we have an API client with three methods,
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
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.
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:
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
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
ES6 has two convenient helper functions for creating Promises out of ordinary values:
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
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:
Another convenient helper function provided by ES6 (though one I honestly haven’t used much) is
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.
The most common way to catch errors is to add a
.catch() block, which will catch an error in any of the preceding
catch() block is triggered if either
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.
getItem() fails, we intervene and create a new item.
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
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.
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
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
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
undefined, but for now we just have to be careful to
return any Promises we create.
.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
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
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
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
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
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.
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: