If you are into web development, there is a 100% chance you have used at least some async functions. There are different ways to use async functions, such as .then()
and async/await.
But what if I told you there are even better ways to use async functions that can reduce request times by up to half? đ€Ż
Yes, it's true! The JavaScript runtime provides various features that we do not commonly know or use. One of these features is the static methods for the Promise class.
In this short blog post we will look at how we can use these to improve our async function calls.
Promise.all()
The Promise.all()
method takes an iterable of promises as an input and returns a single promise that resolve to the array of results of the input promises. It rejects immediately if any input promises reject or if a non-promise throws an error, and will reject with the first rejection method.
Here is an example:
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve,reject)=>{
setTimeout(resolve,100,'foo');
})
Promise.all([promise1,promise2,promise3]).then((values)=>{
console.log(values);
})
// expected output Array [3,42,'foo']
Now let's see how we can use it to speed up our async calls:
Sequential vs Concurrent Execution
Normally, when making asynchronous function calls one after another, each request is blocked by the request before it, this pattern is also known as "waterfall" pattern, as each request can only begin once the previous request has returned some data.
Sequential Execution Pattern.
// Simulate two API calls with different response times
function fetchFastData() {
return new Promise(resolve => {
setTimeout(() => {
resolve("Fast data");
}, 2000);
});
}
function fetchSlowData() {
return new Promise(resolve => {
setTimeout(() => {
resolve("Slow data");
}, 3000);
});
}
// Function to demonstrate sequential execution
async function fetchDataSequentially() {
console.log("Starting to fetch data...");
const startTime = Date.now();
// Start both fetches concurrently
const fastData = await fetchFastData();
const slowData = await fetchSlowData();
const endTime = Date.now();
const totalTime = endTime - startTime;
console.log(`Fast data: ${fastData}`);
console.log(`Slow data: ${slowData}`);
console.log(`Total time taken: ${totalTime}ms`);
}
fetchDataSequentially()
/*
expected output:
Starting to fetch data...
Fast data: Fast data
Slow data: Slow data
Total time taken: 5007ms
*/
Here is a diagram for better visualization
Using Promise.all()
we can fire off all the requests at once and then wait for all of them to finish, this way as the requests do not have to wait for the previous one to finish they can start early and hence get resolved early. Promise.all()
returns an array with resolved promises once all the promises passed to it are resolved.
Here is how we can improve our fetchData
function using promises.
Concurrent Execution Pattern
async function fetchDataConcurrently() {
console.log("Starting to fetch data...");
const startTime = Date.now();
// Start both fetches concurrently
const fastDataPromise = fetchFastData();
const slowDataPromise = fetchSlowData();
// Wait for both promises to resolve
const [fastData, slowData] = await Promise.all([fastDataPromise, slowDataPromise]);
const endTime = Date.now();
const totalTime = endTime - startTime;
console.log(`Fast data: ${fastData}`);
console.log(`Slow data: ${slowData}`);
console.log(`Total time taken: ${totalTime}ms`);
}
/*
expected output:
Starting to fetch data...
Fast data: Fast data
Slow data: Slow data
Total time taken: 3007ms
*/
Here is an diagram for better visualization
We pass all the promises in an array to Promise.all() and then await it. If there are multiple requests, we can save a lot of time this way.
There is one thing to consider, though: what if one promise rejects? If we use Promise.all()
in this case, it will only reject with the rejected promise. What if we want to get result of all the promises be it resolved or rejected??
To handle this case, we can use Promise.allSettled()
. Let's learn about it as well.
Promise.allSettled()
It is a sligt variation of Promise.all()
, the difference is Promise.allSettled()
always resolves, whether the promises passe to it resolves or rejects, it returns with the array containing the results of the promises passed to it.
Example:
const promise1 = Promise.reject("failure");
const promise2 = 42;
const promise3 = new Promise((resolve) => {
setTimeout(resolve, 100, 'foo');
});
Promise.allSettled([promise1, promise2, promise3]).then((results) => {
console.log(results);
});
// expected output: Array [
// { status: "rejected", reason: "failure" },
// { status: "fulfilled", value: 42 },
// { status: "fulfilled", value: 'foo' }
// ]
Another useful static method is Promise.race()
, it can be used to implement timeouts for async functions. Let's see how:
Promise.race()
The Promise.race() method returns a promise that fulfills or rejects as soon as any one of the promise passed to it in an array, fulfills or rejects, with a value or reason for the rejection of the promise.
Example
const promise1 = new Promise((resolve,reject)=>{
setTimeout(resolve,500,'one');
})
const promise2 = new Promise((resolve,reject)=>{
setTimeout(resolve,100,'two');
})
Promise.race([promise1,promise2]).then((value)=>{
console.log(value);
// Both resolves but promise2 is faster.
})
// expected output: 'two'
Let's see how we can use it to implement timeouts:
function fetchData() {
return new Promise(resolve => {
setTimeout(() => {
resolve("Fast data");
}, 6000);
});
}
const fetchDataPromise = fetchData();
function fetchDataWithTimeout(promise, duration) {
let timeoutId;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error(`Operation timed out after ${duration} ms`));
}, duration);
});
return Promise.race([
promise,
timeoutPromise
]).finally(()=>{
/*
As pointed out by @vicariousv , if we do not clear the timeout, then
even if our promise resolves before the timeout, the set timeout will
still keep running. The process will end only after the timeout
completes, which is something to consider when using serverless
functionsas we pay for the runtime.
*/
clearTimeout(timeoutId);
})
}
fetchDataWithTimeout(fetchDataPromise,5000).then((result)=>{
console.log(result)
}).catch((error)=>{
console.log(error)
})
/*
expected result:
Too late
*/
This was it for this blog post. If you want to read more about async promises as a whole, check out my other blog post on async promises: Asynchronous JavaScript: The TL;DR Version You'll Always Recall.
Thank you for reading, and I hope you learned something new!
Top comments (18)
With the existence of
Promise.all
, this is definitely the "right" way of doing this, but it isn't technically the only one:As you can probably tell, this approach can lead to much less readable code, which is why a solution like
Promise.all
makes sense.But we can also implement our own
Promise.all
this way:To make the code shorter, this version just writes the results back into the same array and ignores rejected promises, but it shows how there isn't actually any magic behind
Promise.all
.Your
simple_promise_all
isn't equal to Promise.all though, no?Your's doesn't reject if any reject, and it waits for each resolve/reject sequentially, where as Promise.all tries to resolve all of them at the same time.
In your example, with 10 requests that each take approx. 2 seconds to resolve, you would need to wait 20 seconds. With Promise.all it would be 2 seconds.
Yes, as I pointed out, I ignored that feature to keep my example short.
Both approaches resolve all of the promises at the same time, one of them just hides the implementation so you don't really think about how it's done.
It would be 2 seconds (give or take) in both cases.
await
ing a promise doesn't pause the others, so while the loop is waiting for the first one, the other promises are all still running. Once the first one finishes, the other promises will all be finished too or just about to, soawait
ing them is basically just collecting their results.You are right, I brain farted đ
Just for fun, here's a more complete implementation:
If you remove the "p instanceof Promise" check it still works fine, but it also handles the case where "p" is a then-able; that is a promise that doesn't extend Promise. Await can be used on synchronous values without trouble, so no need to branch.
Good point!
A very important note for using timeouts with Promise.race()
When you have a timeout, while the promise may resolve or reject, the timeout will keep running, keeping the execution alive.
So if the success case passes after 1 second and the program's logic ends, the timeout set for 5 seconds will keep going for another 4 seconds and only then will the process' execution end.
This can be really important in serverless Lambdas where you pay for run time.
In the above example, if the fetch API call succeeds after 500ms, the process ends.
Without clearing the timer as in your example, the process would continue for another 9.5 seconds before ending.
(On mobile, forgive any typos/autocorrects)
Definitely something to keep it mind!
A better way could be using effect.website/
Will definitely check it out, Thanks!
I definitely learned something new, Aditya! Thanks for this amazing blog post! Had to bookmark this one! Thanks again! đŻ
I'm really glad, you got to learn!
Thanks for sharing Aditya. Helped me a lot. Nice brief examples as well.
I have learned a lot! Thanks, Aditya!
Very interesting Aditya!
Recently learnt promises and was looking for practical use cases of these methods. This is the best explanation I found. Thanks a lot!!! đđ€
In my opinion async func is grammar in ES-6. So I think it's better method in javascript.