This article introduces the Javascript Promise class, and how to use a Promise to perform asynchronous work. At the end of this post, you’ll have been exposed to the most important components of the Promise API.
Introduced in the ES2015 specification, MDN dryly describes a Promise as:
The Promise object represents the eventual completion (or failure) of an asynchronous operation, and its resulting value.
But… what exactly does that entail? How does it differ from just using callbacks?
Let’s start with a simple example. If I want to perform an operation asynchronously, traditionally I would use setTimeout to do work after the main thread has finished and use a callback parameter to let the caller utilize the results. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Try running this yourself with node, and you’ll see that ‘before…’ and ‘after…’ are printed followed by ‘the task is happening’.
This is perfectly valid code, but it’s just so unnatural to handle asynchronous tasks this way. There’s no standard to which parameter should be the callback, and there’s no standard to what arguments will be passed back to a given callback. Let’s take a look at the same situation using the new Promise class:
1 2 3 4 5 6 7 8 9 10 11 | |
Let’s walk through this. In someAsyncTask, we’re now returning a call to Promise.resolve with our result. We call then on the result of someAsyncTask and then handle the results. Promise.resolve is returning a resolved Promise, which is run asynchronously after the main thread finishes its initial work (the final console.log, in this case).
Immediately, this feels a lot cleaner to me, but this is a really simple example.
Think about a situation where you need to perform multiple asynchronous callbacks that each depend on the results of the last callback. Here’s an example implementation using callbacks;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | |
I think we can all agree that this is not friendly code. What makes a Promise truly special is its natural chainability. As long as we keep returning Promise objects, we can keep calling then on the results:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
Since concatName is dependent on the result of both getFirstName and getLastName, we still do a little bit of nesting. However, our final asynchronous action can now occur on the outside of the nesting, which will take advantage of the last returned result of our Promise resolutions.
Error handling is another can of worms in callbacks. Which return value is the error and which is the result? Every level of nesting in a callback has to either handle errors, or maybe the top-most callback has to contain a try-catch block. Here’s a particularly nasty example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | |
Every callback has to check for an individual error, and if any level mishandles the error (note the lack of a return on error after getFirstName), you’re guaranteed to end up with undefined behavior. A Promise allows us to handle errors at any level with a catch statement:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | |
We return the result of Promise.reject to signify that we have an error. We only need to call catch once. Any then statements from unresolved promises will be ignored. A catch could be inserted at any nesting point, which could give you the ability to continue the chain:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
So far, we’ve been returning Promise objects using resolve and reject, but there’s also the ability to define our own Promise objects with their own resolve and reject methods. Updating the getFirstName variable:
1 2 3 4 5 6 7 | |
We can also run our asynchronous tasks without nesting by using the Promise.all method:
1 2 3 4 5 6 7 8 9 | |
Give Promise.all a list of promises and it will call them (in some order) and return all the results in an array (in the order given) as a resolved Promise once all given promises have been resolved. If any of the promises are rejected, the entire Promise will be rejected, resulting in the catch statement.
Sometimes you need to run several methods, and you only care about the first result. Promise.race is similar to Promise.all, but only waits for one of the given promises to return:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Sometimes, ‘func1’ will be printed, but most of the time ‘func2’ will be printed.
…And that’s the basics! Hopefully, you have a better understanding of how a Promise works and the advantages provided over traditional callback architectures. More and more libraries are depending on the Promise class, and you can really clean up your logic by using those methods. As Javascript continues to evolve, hopefully we find ourselves getting more of these well-designed systems to make writing code more pleasant.