DEV Community

Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

JS Promises: race vs all vs allSettled

The what, why, and when

Promises have been around for a while now, but up until ES6, we were forced to use them through a third-party library, and the implementations and APIs varied a bit from one another. Thankfully, ES6 came and standardized the API by implementing a native Promise object, allowing everyone to ditch the third-party implementations.

That being said, maybe you were like me and because it required a third-party library, you were ignoring promises and relying on callbacks and libraries such as async.js to deal with that code and avoid running into callback hell (or the pyramid of doom as it is also known).

But now that promises are a native construct, there is really no excuse to ignore them anymore. So in this article, I want to cover three methods that’ll help you deal with some more complex use cases while also dealing with multiple promises at once.

But first, I want to cover one of the main benefits that the promise-based syntax brings to the table.

LogRocket Free Trial Banner

Declarative programming

Through the process of 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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

This is a very simple example, but it shows the power behind Declarative Programming.

A 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 since when 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.

Another benefit of writing code this way is you start thinking about transformations, or steps, that your data needs to go through.

Let me show you:

authenticateUser(usrname, pwd, (err, isAuth) => {
    if(err) return dealWithYourErrors(err);
    if(!isAuth) return dealWithUnauthorizedAccess(usrname);
    getSessionToken(usrname, (err, token) => {
        if(err) return dealWithYourErrors(err);
        loadUserDetails(usrname, (err, details) => {
            if(err) retun dealWithYourErrors(err);
            let user = new User(usrname, token, details);
            performAction(user, (err, result) => { //this is what you wanted to do all along
                if(err) return dealWithYourErrors(err);
                sendBackResponse(result);
            })
        })
    })
})
Enter fullscreen mode Exit fullscreen mode

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 since they don’t require the results of each other to perform their operations.

Sadly, doing it 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 how to deal with serial calls. You (the developer working on this) need to think about these steps while writing them to ensure the correct behavior.

Let me show you how a promise-based approach would be written:

authenticateUser(username, pwd)
    .then( preActions )
    .then( performAction )
    .catch(dealWithYourErrors);
Enter fullscreen mode Exit fullscreen mode

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(usr, pwd){ //main function called by the developer
    return new Promise( (resolve, reject) => {
        //auth logic goes here...
        resolve(usr); //assuming usr and pwd are valid...
    })
}
/** once logged in, we'll need to get the session token and load the user's details
*/
function preActions(usrname) { 
    return Promise.all([getSessionToken(usrname), loadUserDetails(usrname)]);
}

function getSessionToken(usrname) {
    return new Promise( (resolve, reject) => {
        //logic for getting the session token
        resolve("11111")
    })
}
function loadUserDetails(usrname) {
    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 recevied
    console.log(arguments);
}
function dealWithYourErrors(err) {
    console.error(err);
}
Enter fullscreen mode Exit fullscreen mode

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 called
  • The others are simply returning the promises

The above example is the perfect transition into the first method I want to cover: all.

The Promise.all method

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:

  • All of them resolve, which would, in turn, resolve the promise returned by the method
  • One of them fail, which would immediately reject the promise returned

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 and 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 method to fail, halting the search process and preventing from returning a valid search result to your user.

It is here, where you want your internal logic to allow all your promises to be executed, ignoring possible rejections along the way.

Enter Promise.allSettled

This is the solution to all your problems if you’re coming from a use case like the ones above. Sadly, this method is not yet part of the JavaScript. Let me explain: it is a proposed addition that is being considered and reviewed. But sadly, is not a native part of the language just yet.

That being said, given the number of external implementations out there, I thought about covering it anyways.

The gist of it is that unlike the previous method, this one 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:

  1. The status of the returned promised (either ‘rejected’ or ‘fulfilled’)
  2. The value of the fulfilled promise or the reason the in case of a rejected promise
var allSettled = require('promise.allsettled');

var resolved = Promise.resolve(42);
var rejected = Promise.reject(-1);

allSettled([resolved, rejected]).then(function (results) {
    assert.deepEqual(results, [
        { status: 'fulfilled', value: 42 },
        { status: 'rejected', reason: -1 }
    ]);
});

allSettled.shim(); // will be a no-op if not needed

Promise.allSettled([resolved, rejected]).then(function (results) {
    assert.deepEqual(results, [
        { status: 'fulfilled', value: 42 },
        { status: 'rejected', reason: -1 }
    ]);
});
Enter fullscreen mode Exit fullscreen mode

The above example shows the implementation running, it’s a third-party library promise.allsettled mind you, but it complies with the latest version of the Spec.

Note: 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.

What if you wanted to stop at the first resolved promise?

What if instead of stopping once the first promise fails (much like Promise.all does) you wanted to stop once the first one resolves.

This is the other way that the Promise object allows you to deal with multiple promises, by using the race method, which, instead of trying to resolve all promises, actually just waits for the first one to finish, and either fails or succeeds based on whether the promise was resolved or rejected.

Yeah, I kind of cheated a bit there with the title, because this method will also stop the process if the first thing to happen is a rejected promise (just like Promise.all).

But pay no attention to that, let’s think about why you’d want to have several promises running in parallel and only take the result from the first one that gets settled.

When do you use race?

There are, believe or not, several examples of why you’d want to use this method. Let me give you two for now:

Número 1: Performance checks

If, for instance, performance was an important part of your platform, you might want to have several copies of the data source and you could try to query them all hoping to get 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, since you would have to deal with the logic to understand who 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 request = require("request");

let sources = ["http://www.bing.com", "http://www.yahoo.com", "http://www.google.com" ];

let checks = sources.map( s => {
  return new Promise( (res, rej) => {
    let start = (new Date()).getTime()
    request.get(s, (err, resp) => {
        let end = (new Date()).getTime()
        if(err) return rej(err)
        res({
            datasource: s,
            time: end - start
        })
    })
  })
})

Promise.race(checks).then( r => {
  console.log("Fastest source: ", r.datasource, " resolved in: ", r.time, " ms")
})
Enter fullscreen mode Exit fullscreen mode

Yes, the code is a bit basic, and there are probably many ways for you to improve it, but it shows my point. I’m checking which data source is fastest for me 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.

Number 2: Loading indicator, should I show it?

Another example where you might want to consider using this 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. And loading times might depend on too many things for you to create a rule to know 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);
Enter fullscreen mode Exit fullscreen mode

Now the race is against an actual asynchronous request and a timeout set as a limiter. Now the logic to decide whether to show or not the loading indicator is hidden behind the race method.

Final thoughts

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!


Plug: LogRocket, a DVR for web apps

 
LogRocket Dashboard Free Trial Banner
 
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
Try it for free.


The post JS Promises: race vs all vs allSettled appeared first on LogRocket Blog.

Latest comments (1)

Collapse
 
mhnpd profile image
Mohan Upadhyay • Edited

Great post. Recently I was stuck on a problem where I want to download multiple files (specially videos) from s3 using aws-sdk for JS. It's on election app. Basically all the data required to download content will be receive from a queue which is implement using rabbitmq. Now when I receive new object from queue I want to cancel all the pending promises and download request (xhr request). It there any suggestions for such cases. I am not still able to figure out proper solution for this though consultant in a lot of places.