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: