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.
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]
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.
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);
})
})
})
})
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);
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);
}
Here are the highlights from the above code:
-
preActions
calls both functions in parallel, using theall
method for the nativePromise
object. If any of them were to fail (thus rejecting their respective promise), then the entire set would fail and thecatch
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:
- The status of the returned promised (either ‘rejected’ or ‘fulfilled’)
- 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 }
]);
});
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")
})
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);
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 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.
Top comments (1)
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.