Improving Performance of Async Operations in JavaScript.
If you’re reading this, you’ve probably worked with promises before. And when I say promises, I don’t mean the ones made by your friend or partner. Most of the time, they’re API calls on the client or database queries on the server (if you write JavaScript).
The Problem With Sequential Execution
Let’s look at an example that uses sequential execution to understand what the problem is. Here’s an example function that gets the user data. The example is highly abstracted for simplicity.
async function getUserData(userId: string): Promise<UserData> {
const posts = await getPosts(userId); // 500ms
const followers = await getFollowers(userId); // 1000ms
const following = await getFollowing(userId); // 1000ms
return { posts, followers, following };
}
const data = await getUserData(userId); // Time to get user data 2.5s
At first glance, it’s tough to spot what’s wrong here. But technically, each request waits for the previous one to finish before starting, which leads to higher delays. Unless your server responds within 100ms for each request across the globe, this delay could be even worse for people living far away from your database. So, what can we do?
For starters, we could place more databases closer to your users. Umm, that doesn’t sound very practical—unless you have a lot of VC money to burn on infra.
The second thing we can do is simple: read the docs. The docs about Promises. It has a hidden gem, and it’s called Promise.all().
What is Promise.all()?
Promise.all() runs all your promises concurrently. Which means if you have multiple promises, such as above, they all can be fired concurrently instead of waiting for each one to complete. Here’s a refactored example that uses Promise.all() to fire promises concurrently.
async function getUserData(userId: string): Promise<UserData> {
const [posts, followers, following] = await Promise.all([
getPosts(userId), // 500ms
getFollowers(userId), // 1000ms
getFollowing(userId), // 1000ms
]);
return { posts, followers, following };
}
const data = await getUserData(userId); // Time to get user data 1s
Handling Errors with Promise.all()
One caveat with Promise.all() is that it fails fast—if any promise rejects, the entire operation fails. You can handle this by wrapping each promise in a .catch()
block:
async function getUserData(userId: string): Promise<UserData> {
const [posts, followers, following] = await Promise.all([
getPosts(userId).catch(() => []),
getFollowers(userId).catch(() => []),
getFollowing(userId).catch(() => []),
]);
return { posts, followers, following };
}
This ensures that a single failure doesn’t disrupt the entire batch, allowing partial results to still be processed.
When Not to Use Promise.all()
While Promise.all() is great for independent tasks, it’s not the best fit for dependent queries. If the result of one promise is required for the next, sequential execution is necessary. Here’s an example:
async function getOrderDetails(request: Request): Promise<OrderDetails> {
const user = await getUser(request);
const orders = await getOrders(user.id); // here, user.id is required so Promise.all() can't be used
const orderDetails = await getOrderDetailsById(orders[0].id); // here, orders[0].id is required so Promise.all() can't be used
return { user, orders, orderDetails };
}
const details = await getOrderDetails(userId);
In this case, each step depends on the previous one. Using Promise.all() here would result in errors since the dependent data wouldn’t be available when needed.
For such scenarios, chaining promises or using async/await
sequentially is more appropriate. Here’s a benchmark to show the difference between Promise.all() and sequential execution.
Takeaways
- Using Promise.all() is an good way to improve the performance of asynchronous operations by running them concurrently. It’s especially useful when the operations are independent of each other and don’t rely on sequential execution.
- Use Promise.all() to execute multiple independent promises concurrently, reducing execution time.
- Handle errors gracefully with
.catch()
blocks or consider Promise.allSettled() for detailed error handling. - Avoid Promise.all() for dependent tasks where sequential execution is required.
Experiment with Promise.all() in your next project, and let your async tasks soar!