Editor’s note: This post was updated on 8 November 2021 to improve the coding tutorial, address changes to JavaScript Promises that have occurred since 2019, and discuss their beneficial use cases more thoroughly.
Promises in JavaScript are used to handle asynchronous operations. A promise is an object representing the eventual completion or failure of an asynchronous operation. Before they were natively introduced with ES2015, developers had to rely on passing callbacks into functions or third-party libraries to use promises in JavaScript.
Before the adoption of promises, libraries like async.js were used to work with asynchronous code. Now, the native Promise object can be used without having to rely on third-party implementations. With this, we can avoid installing third-party promises or relying on callbacks, and running into callback hell.
As promises are now a native construct, they are much more approachable. In this article, I want to cover the methods that’ll help you deal with some more complex use cases, while also dealing with multiple promises at once. These methods are:
Promise.all()
Promise.race()
Promise.allSettled()
Promise.prototype.catch()
But first, I want to cover one of the main benefits that the promise-based syntax brings to the table.
When using the method-chaining syntax and the logic behind the method names (i.e., then
and catch
), one can construct a block of code that focuses on declaring the intent for it, instead of actually specifying how it needs to do what we need.
Let me explain. What if you wanted to grab every number inside a list and double it? How would you go about it?
The way we usually learn to write that code is to think like the computer: You need to iterate over every item in the list, so you’ll need a position counter, which needs to go from 0 to the amount of numbers in the array, and for every number, you need to double it, and possibly add it into another different array.
Which translates to:
let list = [1,2,3,4,5]; let results = [] for(let counter = 0; counter < list.length; counter++) { results[i] = list[i] * 2; } console.log(results); //[2,4,6,8,10]
Now, what I propose is to instead think about what needs to happen and write that. In other words: Map every number to its double.
let list = [1,2,3,4,5]; let results = list.map( i => i * 2 ); console.log(results); //[2,4,6,8,10]
This is a very simple example, but it shows the power behind declarative programming.
This simple change in your approach can help you write cleaner, easier-to-read code. The cognitive load behind reading the second example is considerably lower than the first one because when you’re using the for
loop, you have to mentally parse the code and execute it line by line, while the map
is something you can quickly interpret at a higher level.
The same applies to writing code using promises. In a scenario where you’re not using promises and have to pass multiple callback functions to be able to respond to multiple events, the code becomes complex and hard to read.
With promise methods like Promise.all()
— which takes an array of promises as an argument a returns a promise that resolves when all the passed promises have been resolved — and the concept of chaining promises, you can write cleaner and easier to read code. Using promises, you can visualize the steps your data goes through in a more concise and straightforward way, unlike using callbacks.
Let me show you:
authenticateUser(username, pwd, (err, isAuth) => { if(err) return dealWithYourErrors(err); if(!isAuth) return dealWithUnauthorizedAccess(username); getSessionToken(username, (err, token) => { if(err) return dealWithYourErrors(err); loadUserDetails(username, (err, details) => { if(err) return dealWithYourErrors(err); let user = new User(username, token, details); performAction(user, (err, result) => { //this is what you wanted to do all along if(err) return dealWithYourErrors(err); sendBackResponse(result); }) }) }) })
The above is a classic example of nested callbacks, where you have several pieces of information that need to be taken from different services (or in different steps, due to some other logic).
By default, callbacks only let you deal with asynchronous behavior serially, which, in this case, is not ideal. Both getSessionToken
and loadUserDetails
could be done in parallel because they don’t require each other’s results to perform their operations. Sadly, running getSessionToken
and loadUserDetails
in parallel would require some extra code, such as using async.js or writing your own logic.
Furthermore, the code’s entire structure is imperative in the sense that it’s explicitly stating how to deal with errors and serial calls. You (the developer working on this) need to think about these steps while writing them to ensure the correct behavior.
But a promise-based approach would be written the following way:
authenticateUser(username, pwd) .then( preActions ) .then( performAction ) .catch(dealWithYourErrors);
I’m sure we can all agree that is a lot simpler to write and to read. Let me show you a mocked implementation of these functions since promises need to be returned in all of them:
function authenticateUser(user, pwd){ //main function called by the developer return new Promise( (resolve, reject) => { //auth logic goes here... resolve(user); //assuming user and pwd are valid... }) } /** once logged in, we'll need to get the session token and load the user's details */ function preActions(username) { return Promise.all([getSessionToken(username), loadUserDetails(username)]); } function getSessionToken(username) { return new Promise( (resolve, reject) => { //logic for getting the session token resolve("11111") }) } function loadUserDetails(username) { return new Promise( (resolve, reject) => { //here is where you'd add the logic for getting the user's details resolve({name: 'Fernando'}); }) } function performAction() { //the actual action: we're just logging into stdout the arguments received console.log(arguments); } function dealWithYourErrors(err) { console.error(err); }
Here are the highlights from the above code:
preActions
calls both functions in parallel, using the all
method for the native Promise
object. If any of them were to fail (thus rejecting their respective promise), then the entire set would fail and the catch
method would’ve been calledThe above example is the perfect transition into the first method I want to cover: all
.
Perfect for when you’re having to deal with multiple parallel asynchronous calls, the all
method allows you to have your cake and eat it too.
By definition, Promise.all
will run all your promises until one of the following conditions are met:
The thing to remember with Promise.all
is that last bullet point: you can’t handle partial failures. If one of the promises is rejected, then the entire process is halted and the failure callback is called. This is not ideal if the rejected promise is not doing something mission-critical and its content could potentially be missing.
Think about a search service that is getting the data from the main database and using external services to enrich the results. These external services aren’t required — they’re just there to help you provide more information, if available.
Having these third-party services fail during the search process would cause this entire method to fail, halting the search process and preventing the return of a valid search result to your user.
It is here that you want your internal logic to allow all your promises to be executed, ignoring possible rejections along the way.
Promise.all
failures with Promise.allSettled()
Promise.allSettled()
is the solution to all your problems if you’re coming from a use case like the ones above. This method was a proposed addition to the JavaScript spec and has now been added to the promise object.
The gist of the Promise.allSettled()
method is that unlike the previous method, Promise.all()
, this will not fail once the first promise is rejected. Instead, it’ll return a list of values. These values will be objects, with two properties:
rejected
or fulfilled
)The below example shows the implementation running.
var resolved = Promise.resolve(42); var rejected = Promise.reject(-1); Promise.allSettled([resolved, rejected]).then(function (results) { assert.deepEqual(results, [ { status: 'fulfilled', value: 42 }, { status: 'rejected', reason: -1 } ]); }); Promise.allSettled([resolved, rejected]).then(function (results) { assert.deepEqual(results, [ { status: 'fulfilled', value: 42 }, { status: 'rejected', reason: -1 } ]); });
Don’t let the name of the method confuse you, many people think “allSettled” means the same as “allResolved”, which is not correct. A promise is settled once it gets either resolved or rejected — otherwise, it’s pending. Check out the full list of states and fates a promise can have for more details.
Promise.race()
to implement multiple promisesThe race
method is another way the promise object allows you to work with multiple promises. The Promise.race()
method returns a promise that fulfills or rejects as soon as one of the promises in an iterable array is fulfilled or rejected. This array of promises is passed as an argument to the method.
When any one of the promises passed into the method is settled (i.e., either fulfilled or rejected, but not pending), the method returns a promise that fulfills or rejects with the value or reason from that promise.
The Promise.race()
method is similar to Promise.all()
, but the major difference is that Promise.race
does not wait for all promises to be resolved before returning a resolved promise.
Let’s talk about why you’d want to have several promises running in parallel and only take the result from the first one that gets settled.
Promise.prototype.catch()
The Promise.prototype.catch()
method is very handy for responding to rejected promises.
For example:
const promise1 = new Promise((resolve, reject) => { setTimeout(() => { reject("Promise has been rejected") }, 3000) }); promise1.catch((reason) => { console.error(reason); });
Here, promise1
is a new promise that is rejected after three seconds. promise1.catch()
is then called, which runs a function passed to it as an argument. This function, in turn, has one argument — reason
— which is the reason for the rejection.
The promise returned by catch()
is rejected if onRejected
throws an error or returns a promise that is rejected; otherwise, the promise returned by catch()
is resolved.
Since the catch()
method returns a Promise, it can be chained. Looking again at the previous example, another catch()
method can be chained to catch any further errors or rejections:
promise1.catch((reason) => { console.error(reason); throw "Error" }).catch(reason => console.log(reason))
race
?There are several examples of why you’d want to use the race
method. Let’s look at two for now:
If, for instance, performance is an important part of the project you’re building, you may want to have several copies of the data source so that you can try to query them all in search of getting the fastest one, depending on network traffic or other external factors.
You could do it without promises, but again, there would be an added expense to this approach because you’d have to deal with the logic to understand which promise returned first and what to do with the other pending requests.
With promises and the race
method, you can simply focus on getting the data from all your sources and let JavaScript deal with the rest.
const fetch = require('node-fetch'); // sources for data let sources = ["https://catfact.ninja/fact", "https://www.boredapi.com/api/activity"]; // map through the sources and create and // return a Promise for each request to the data source // creating a new array of promises let checks = sources.map(source => { return new Promise((resolve, reject) => { // get the start time let start = (new Date()).getTime(); fetch(source) .then(res => res.json()) .then(data => { // send an object of the data, the data source(URL) and time elapsed resolve({ data: data, source: source, time: (new Date()).getTime() - start }); }) .catch(err => { reject(err); }) }) }) // run the Promise.race() method on the newly created array of promises Promise.race(checks).then(check => { // log out data returned by the first promise to resolve console.log(check); })
The code example above shows how you can create an array of promises by mapping through an array of source URLs.
Within the map
method, a new promise is created to fetch the data from the data source URL and return the data, source URL, and amount of time elapsed if the promise is resolved in resolve()
.
If there are any errors from the catch()
method, the promise is rejected and returns the error reject(err)
.
Essentially, I’m checking which data source is fastest without having to add any particular logic to deal with asynchronous resolutions. If I wanted to compare results, I would have to change this for a Promise.allSettled
call instead.
Another example of where you may want to consider using the race
method is when trying to decide whether or not to display a loading indicator in your UI. A good rule of thumb when creating SPAs is that your asynchronous calls should trigger a loading indicator for the user, to let them know something is happening.
But this rule is not ideal when the underlying request happens very quickly, because all you’ll probably get in your UI is a flicker of a message, something that goes by too fast. Also, loading times sometimes depend on too many factors for you to be able to create a rule for when to show the indicator and when to simply do the request without it.
You can play around with the concepts of rejection and resolution to have something like this:
function yourAsynchronousRequest(params) { return new Promise((resolve, reject) => { //here is your request code, it'll resolve once it gets the actual data from the server }); } function showDataToUser(params) { return yourAsynchronousRequest(params).then( data => console.log("data fetched:", data)); } function timeout() { return new Promise((resolve, reject) => { setTimeout(() => reject(), TIMEOUTLIMIT); //TIMEOUTLIMIT is a constant you configured }); } function showLoadingIndicator() { console.log("please wait...") } Promise.race([showDataToUser(), timeout()]).catch(showLoadingIndicator);
Now the race is against an actual asynchronous request and a timeout is set as a limiter. The logic for deciding whether or not to show the loading indicator is hidden behind the race
method.
Here, an asynchronous request and a timeout
function is passed to the race()
method. The asynchronous function resolves as soon as it’s able to get the actual data from the server.
The timeout
function, on the other hand, rejects the promise after a specified amount of time.
This will prevent the showLoadingIndicator()
function from running immediately until the set time has elapsed.
This way, if the promise returned by showDataToUser()
resolves before the timeout
elapses and rejects the Promise, the user data will be displayed. If not and the promise has been rejected, .catch
will be used to run the showLoadingIndicator()
.
Promises are fun, and ignoring them was not one of my best moves back in the day, so I’m super glad I’ve decided to incorporate them into my daily coding habits, and if you haven’t yet, I strongly suggest you do it as well.
Let me know in the comments if you’re using these methods, and I’m especially interested in what kind of use cases you have for the Promise.race
method, I really want to know!
See you on the next one!
Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowAngular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.
Fix sticky positioning issues in CSS, from missing offsets to overflow conflicts in flex, grid, and container height constraints.
From basic syntax and advanced techniques to practical applications and error handling, here’s how to use node-cron.
The Angular tree view can be hard to get right, but once you understand it, it can be quite a powerful visual representation.
3 Replies to "JavaScript Promises: race, all, allSettled, and then"
Mozilla’s web documentation at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all has an good workaround for the problem of rejection stopping Promise.all(). I have been using it without any issues so far:
var p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(‘p1_delayed_resolution’), 1000);
});
var p2 = new Promise((resolve, reject) => {
reject(new Error(‘p2_immediate_rejection’));
});
Promise.all([
p1.catch(error => { return error }),
p2.catch(error => { return error }),
]).then(values => {
console.log(values[0]) // “p1_delayed_resolution”
console.log(values[1]) // “Error: p2_immediate_rejection”
})
What is the difference between this logic and Promise.allSettled ?
Nice article, but one thing should be noted. You say:
“By definition,Promise.all will run all your promises until one of the following conditions are met:”
It’s not true. Promise.all doesn’t run anything. Promises are ALWAYS fired immediately, always. There are no methods to explicitly control Promise’s execution. Promise.all just listens their execution, but doesn’t do anything else.