This post is intended to be the ultimate JavaScript Promises
tutorial: recipes and examples for everyday situations (or
that’s the goal 😉). We cover all the necessary methods like
then
, catch
, and finally
.
Also, we go over more complex situations like executing promises
in parallel with Promise.all
, timing out APIs with
Promise.race
, promise chaining and some best
practices and gotchas.
NOTE: I’d like this post to be up-to-date with the most common use cases for promises. If you have a question about promises and it’s not answered here. Please, comment below or reach out to me directly @iAmAdrianMejia. I’ll look into it and update this post.
Related Posts:
JavaScript Promises
A promise is an object that allows you to handle asynchronous operations. It’s an alternative to plain old callbacks.
Promises have many advantages over callbacks. To name a few:
- Make the async code easier to read.
- Provide combined error handling.
- Better control flow. You can have async actions execute in parallel or series.
Callbacks tend to form deeply nested structures (a.k.a. Callback hell). Like the following:
1 |
a(() => { |
If you convert those functions to promises, they can be chained producing more maintainable code. Something like this:
1 |
Promise.resolve() |
As you can see, in the example above, the promise object exposes
the methods .then
and .catch
. We are
going to explore these methods later.
How do I convert an existing callback API to promises?
We can convert callbacks into promises using the Promise constructor.
The Promise constructor takes a callback with two arguments
resolve
and reject
.
- Resolve: is a callback that should be invoked when the async operation is completed.
- Reject: is a callback function to be invoked when an error occurs.
The constructor returns an object immediately, the promise
instance. You can get notified when the promise is “done” using
the method .then
in the promise instance. Let’s see
an example.
Wait, aren’t promises just callbacks?
Yes and no. Promises are not “just” callbacks, but they do use
asynchronous callbacks on the .then
and
.catch
methods. Promises are an abstraction on top
of callbacks that allows you to chain multiple async operations
and handle errors more elegantly. Let’s see it in action.
Promises anti-pattern (promise hell)
Before jumping into how to convert callbacks to promises, let’s see how NOT to it.
Please don’t convert callbacks to promises from this:
1 |
a(() => { |
To this:
1 |
a().then(() => { |
Always keep your promises as flat as you can.
It’s better to do this:
1 |
a() |
Let’s do some real-life examples! 💪
Promesifying Timeouts
Let’s see an example. What do you think will be the output of the following program?
const promise = new Promise((resolve, reject) => { setTimeout(() => { resolve('time is up ⏰'); }, 1e3); setTimeout(() => { reject('Oops 🔥'); }, 2e3); }); promise .then(console.log) .catch(console.error);
Is the output:
1 |
time is up ⏰ |
or is it
1 |
time is up ⏰ |
?
It’s the latter, because
When a promise it’s
resolve
d, it can no longer bereject
ed.
Once you call one method (resolve
or
reject
) the other is invalidated since the promise
in a settled state. Let’s explore all the different
states of a promise.
Promise states
There are four states in which the promises can be:
- ⏳ Pending: initial state. Async operation is still in process.
-
✅ Fulfilled: the operation was successful.
It invokes
.then
callback. E.g.,.then(onSuccess)
. -
⛔️ Rejected: the operation failed. It
invokes the
.catch
or.then
‘s second argument (if any). E.g.,.catch(onError)
or.then(..., onError)
-
😵 Settled: it’s the promise final state. The
promise is dead. Nothing else can be resolved or rejected
anymore. The
.finally
method is invoked.
Promise instance methods
The Promise API exposes three main methods: then
,
catch
and finally
. Let’s explore each
one and provide examples.
Promise then
The then
method allows you to get notified when the
asynchronous operation is done, either succeeded or failed. It
takes two arguments, one for the successful execution and the
other one if an error happens.
1 |
promise.then(onSuccess, onError); |
You can also use catch to handle errors:
1 |
promise.then(onSuccess).catch(onError); |
Promise chaining
then
returns a new promise so you can chain
multiple promises together. Like in the example below:
1 |
Promise.resolve() |
Promise.resolve
immediately resolves the promise as
successful. So all the following then
are called.
The output would be
1 |
then#1 |
Let’s see how to handle errors on promises with
then
and catch
.
Promise catch
Promise .catch
the method takes a function as an
argument that handles errors if they occur. If everything goes
well, the catch method is never called.
Let’s say we have the following promises: one resolves or rejects after 1 second and prints out their letter.
1 |
const a = () => new Promise((resolve) => setTimeout(() => { console.log('a'), resolve() }, 1e3)); |
Notice that c
simulates a rejection with
reject('Oops!')
1 |
Promise.resolve() |
The output is the following:
In this case, you will see a
, b
, and
the error message on c
. The function` will never
get executed because the error broke the sequence.
Also, you can handle the error using the 2nd argument of the
then
function. However, be aware that
catch
will not execute anymore.
1 |
Promise.resolve() |
As you can see the catch doesn’t get called because we are
handling the error on the .then(..., onError)
part.
d
is not being called regardless. If you want to
ignore the error and continue with the execution of the promise
chain, you can add a catch
on c
.
Something like this:
1 |
Promise.resolve() |
Now’dgets executed!! In all the other cases, it didn't. This
early
catch` is not desired in most cases; it can lead to things
falling silently and make your async operations harder to debug.
Promise finally
The finally
method is called only when the promise
is settled.
You can use a .then
after the .catch
,
in case you want a piece of code to execute always, even after a
failure.
1 |
Promise.resolve() |
or you can use the .finally
keyword:
1 |
Promise.resolve() |
Promise class Methods
There are four static methods that you can use directly from the
Promise
object.
- Promise.all
- Promise.reject
- Promise.resolve
- Promise.race
Let’s see each one and provide examples.
Promise.resolve and Promise.reject
These two are helper functions that resolve or reject
immediately. You can pass a reason
that will be
passed on the next .then
.
1 |
Promise.resolve('Yay!!!') |
This code will output Yay!!!
as expected.
1 |
Promise.reject('Oops 🔥') |
The output will be a console error with the error reason of
Oops 🔥
.
Executing promises in Parallel with Promise.all
Usually, promises are executed in series, one after another, but you can use them in parallel as well.
Let’s say are polling data from 2 different APIs. If they are
not related, we can do trigger both requests at once with
Promise.all()
.
For this example, we will pull the Bitcoin price in USD and
convert it to EUR. For that, we have two independent API calls.
One for BTC/USD and other to get EUR/USD. As you imagine, both
API calls can be called in parallel. However, we need a way to
know when both are done to calculate the final price. We can use
Promise.all
. When all promises are done, a new
promise will be returning will the results.
const axios = require('axios'); const bitcoinPromise = axios.get('https://api.coinpaprika.com/v1/coins/btc-bitcoin/markets'); const dollarPromise = axios.get('https://api.exchangeratesapi.io/latest?base=USD'); const currency = 'EUR'; // Get the price of bitcoins on Promise.all([bitcoinPromise, dollarPromise]) .then(([bitcoinMarkets, dollarExchanges]) => { const byCoinbaseBtc = d => d.exchange_id === 'coinbase-pro' && d.pair === 'BTC/USD'; const coinbaseBtc = bitcoinMarkets.data.find(byCoinbaseBtc) const coinbaseBtcInUsd = coinbaseBtc.quotes.USD.price; const rate = dollarExchanges.data.rates[currency]; return rate * coinbaseBtcInUsd; }) .then(price => console.log(`The Bitcoin in ${currency} is ${price.toLocaleString()}`)) .catch(console.log);
As you can see, Promise.all
accepts an array of
promises. When the request for both requests are completed, then
we can proceed to calculate the price.
Let’s do another example and time it:
1 |
const a = () => new Promise((resolve) => setTimeout(() => resolve('a'), 2000)); |
How long is it going to take to solve each of these promises? 5 seconds? 1 second? Or 2 seconds?
You can experiment with the dev tools and report back your results ;)
Promise race
The Promise.race(iterable)
takes a collection of
promises and resolves as soon as the first promise settles.
1 |
const a = () => new Promise((resolve) => setTimeout(() => resolve('a'), 2000)); |
What’s the output?
It’s b
! With Promise.race
only the
fastest gets to be part of the result. 🏁
You might wonder: _ What’s the usage of the Promise race?_
I haven’t used it as often as the others. But, it can come handy in some cases like timing out promises and batching array of requests.
Timing out requests with Promise race
1 |
Promise.race([ |
If the request is fast enough, then you have the result.
1 |
Promise.race([ |
Promises FAQ
This section covers tricks and tips using all the promises methods that we explained before.
Executing promises in series and passing arguments
This time we are going to use the promises API for Node’s
fs
and we are going to concatenate two files:
1 |
const fs = require('fs').promises; // requires node v8+ |
In this example, we read file 1 and write it to the output file.
Later, we read file 2 and append it to the output file again. As
you can see, writeFile
promise returns the content
of the file, and you can use it in the next
then
clause.
How do I chain multiple conditional promises?
You might have a case where you want to skip specific steps on a promise chain. You can do that in two ways.
const a = () => new Promise((resolve) => setTimeout(() => { console.log('a'), resolve() }, 1e3)); const b = () => new Promise((resolve) => setTimeout(() => { console.log('b'), resolve() }, 2e3)); const c = () => new Promise((resolve) => setTimeout(() => { console.log('c'), resolve() }, 3e3)); const d = () => new Promise((resolve) => setTimeout(() => { console.log('d'), resolve() }, 4e3)); const shouldExecA = true; const shouldExecB = false; const shouldExecC = false; const shouldExecD = true; Promise.resolve() .then(() => shouldExecA && a()) .then(() => shouldExecB && b()) .then(() => shouldExecC && c()) .then(() => shouldExecD && d()) .then(() => console.log('done'))
If you run the code example, you will notice that only
a
and d
are executed as expected.
An alternative way is creating a chain and then only add them if
1 |
const chain = Promise.resolve(); |
How to limit parallel promises?
To accomplish this, we have to throttle
Promise.all
somehow.
Let’s say you have many concurrent requests to do. If you use a
Promise.all
that won’t be good (especially when the
API is rate limited). So, we need to develop and function that
does that for us. Let’s call it
promiseAllThrottled
.
1 |
// simulate 10 async tasks that takes 5 seconds to complete. |
The output should be something like this:
So, the code above will limit the concurrency to 3 tasks
executing in parallel. This is one possible implementation of
promiseAllThrottled
using
Promise.race
to throttle the number of active tasks
at a given time:
1 |
/** |
The promiseAllThrottled
takes promises one by one.
It executes the promises and adds it to the queue. If the queue
is less than the concurrency limit, it keeps adding to the
queue. Once the limit is reached, we use
Promise.race
to wait for one promise to finish so
we can replace it with a new one. The trick here is that the
promise auto removes itself from the queue when it is done.
Also, we use race to detect when a promise has finished, and it
adds a new one.
Related Posts: