JavaScript Promises: A Guide to Asynchronous Operations
6 min readNov 18, 2022
Promises
- A
Promise
represents the eventual success value (or failure reason) of an asynchronous operation that hasn’t completed yet. You can think of it as a proxy for a value not necessarily known at the time it is created. - Promises simplify deferred and asynchronous computations by returning an object to which you associate handlers with an asynchronous action’s eventual success value or failure reason.
- This lets asynchronous methods return values like synchronous methods: instead of immediately returning the final value, the asynchronous method returns a promise to supply the value at some point in the future.
Promise states
pending
: initial state, neither fulfilled nor rejected.fulfilled
: the operation was completed successfully.rejected
: the operation, i.e., the action relating to the promise, failed.settled
: has fulfilled or rejected.
When should the Promise
constructor be used?
- To construct a new
Promise
object. - To create a promise for legacy API callbacks and asynchronous tasks like
setTimeout
orXMLHttpRequest
.
Difference between Event listeners and Promises
- A promise can only succeed or fail once. It cannot succeed or fail twice, neither can it switch from success to failure or vice versa. For example:
const promise = new Promise((resolve, reject) => {
resolve("Yay!");
reject("Nevermind");
resolve("Yay again!");
});
promise
.then(res => console.log(res))
.catch(err => console.log(err));
// => 'Yay!'
- If a promise has succeeded or failed and you later add a success/failure callback, the correct callback will be called, even though the event took place earlier. This is extremely useful for async success/failure, because you’re less interested in the exact time something became available, and more interested in reacting to the outcome.
Purpose of Promises — Replacing Callbacks
- Nesting callbacks can often get messy and troublesome. With promises, all you have to do is add another
.then
block. The result is code that is much cleaner to write.
const promise = new Promise(resolve => resolve(1));
promise
.then((num) => {
return num + 1; // num is now 2
})
.then((num) => {
return num + 2; // num is now 4
})
.then((num) => {
return num + 3; // num is now 7
})
.finally(() => {
console.log('Done!');
});
- In the code above, there are 3
then
callbacks which are invoked in the order shown. Each one passes its return value to the next callback in the chain. - When a
Promise
is settled (either resolved or rejected),finally
is invoked. Likethen
andcatch
,finally
returns an equivalentPromise
object that can be chained to another promise method. This lets you avoid duplicating code in thethen
andcatch
handlers. - Note that the callback for
finally
takes no arguments (passingnum
would have have setnum
toundefined
regardless of what happened in thethen
clauses).
Running promises in parallel
- JavaScript doesn’t wait for a
Promise
to finish executing before starting the next one. This allows us to run promises in parallel. - We can then use what’s called
Promise.all
to pass in an iterable array of all the different promises that we want to run, which then returns a singlePromise
that resolves to an array of the results of the input promises.
const connectToServer1 = new Promise ((resolve, reject) => {
resolve('Connected to server 1');
});
const connectToServer2 = new Promise ((resolve, reject) => {
resolve('Connected to server 2');
});
const connectToServer3 = new Promise ((resolve, reject) => {
resolve('Connected to server 3');
});
Promise.all([
connectToServer1,
connectToServer2,
connectToServer3
]).then((messages) => {
console.log(messages);
})
// [ 'Connected to server 1', 'Connected to server 2', 'Connected to server 3' ]
- All promises in
Promise.all
run at the exact same time. For example,connectToServer1
could’ve taken a long time if the server was experiencing a slowdown, while the other 2 could’ve been very quick. The other two won't have to wait for the first one to finish. Promise.all
is going to run every single one of these promises and when it's done, it's going to call the.then
or.catch
method depending on if they resolved or failed.- If all of the promises inside the array resolve, then an array containing all of the successful messages gets output. However, if one or more promises get rejected then the first one that rejects is caught in the
.catch
block and its failure message is output. - For example, let’s say you were using promises to load 3 images. If 2 out of 3 successfully resolve and 1 rejects, then
Promise.all
will immediately reject, causing every other image not to load.
Promise.allSettled()
- The
Promise.allSettled
method returns a singlePromise
object that resolves after all of the given promises have either fulfilled/resolved or rejected. The returnedPromise
object contains an object with two properties for each provided promise:status
andvalue
orreason
.
status
— can be“fulfilled”
or“rejected”
value
— contains the resolved value of a fulfilled promisereason
— contains the reason of a rejected promise
const promise1 = Promise.resolve('foo');
const promise2 = new Promise((_, reject) => setTimeout(reject, 100, 'bar'));
const promises = [promise1, promise2];
Promise.allSettled(promises).then(results => {
results.forEach(result => console.log(result));
});
{ status: 'fulfilled', value: 'foo' }
{ status: 'rejected', reason: 'bar' }
Do something as soon as one asynchronous operation finishes
- Let’s say you wanted to do something as soon as one
Promise
had settled, instead of waiting for all of them to settle before accomplishing some task. - For this purpose, use
Promise.race
instead ofPromise.all
.
Promise.race([
loadImageOne,
loadImageTwo,
loadImageThree
]).then(message => {
console.log(message);
})
- They’re both almost the same, except that
Promise.race
will return as soon as onePromise
is settled. Because of that, it will return a single message as opposed to all of the messages.
Promise.any()
Promise.any
fulfills with the first promise to fulfill (i.e., resolve) even if another promise rejects first. If all of them get rejected, it returns anAggregateError
message.
const promise1 = new Promise((resolve, _) => {
setTimeout(resolve, 500, 'foo');
});
const promise2 = new Promise((_, reject) => {
setTimeout(reject, 100, 'bar');
});
const promises = [promise1, promise2];
Promise.any(promises)
.then(result => console.log(result)) // => foo
.catch(error => console.log(error));
- In contrast,
Promise.race
will fulfill as soon as one promise settles (either resolved or rejected).
Promise.race(promises)
.then(result => console.log(result))
.catch(error => console.log(error)); // => bar
- If all promises were to be rejected,
Promise.any
logs anAggregateError
message that includes the error messages for each:
[AggregateError: All promises were rejected] {
[errors]: [ 'foo', 'bar' ]
}
Promise.any
focuses on retrieving data as fast as possible. By waiting for the first successful response and ignoring failed ones,Promise.any
allows us to create more reliable code. For example, making multiple network requests in case one of them fails. Those savings can be passed onto the user by improving response times.- For example, let’s say you’re trying to retrieve data from some API endpoint and the request fails. You retry a few more times but your reconnect attempts don’t go through. At this point, you can use
Promise.any
and make multiple requests to servers located further away and see if at least one of them is able to provide the data you’re seeking. This would allow you to recover quicker, while trying to figure out what’s causing the slowdown on the main server.
Creating a time limit for an async task
- By racing an async task such as an API call against a promise that is going to be rejected after a set period of time, we effectively create a time limit for the async task.
reject
/resolve
using Promise.race
:
const timeout = new Promise((_, reject) => {
setTimeout(reject, 500, 'error: time limit exceeded');
});
const promise = new Promise((resolve, _) => {
fetch("https://www.boredapi.com/api/activity")
.then(response => response.json())
.then(data => resolve(data.activity));
})
Promise.race([timeout, promise])
.then(msg => console.log(msg))
.catch(msg => console.log(msg))
resolve
/resolve
using Promise.any
:
const timeout = new Promise((resolve, _) => {
setTimeout(resolve, 500, 'too slow');
});
const promise = new Promise((resolve, _) => {
fetch("https://www.boredapi.com/api/activity")
.then(response => response.json())
.then(data => resolve(data.activity));
})
Promise.any([timeout, promise]).then(msg => console.log(msg))
Summary
Promise.all
is useful anytime you have more than one promise and your code wants to know when all the operations that those promises represent have finished successfully.Promise.race
looks for the first settled (rejected or resolved) promise.Promise.any
looks for the first fulfilled promise.