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.