Error handling with Promise.allSettled vs Promise.all

It'll all be fine, I promise!

January 21, 2023

A common performance optimisation that I see often recommended when making multiple async calls in javascript is to wrap all asynchronous calls made within Promise.all

For example, say when a user lands on a product page for your app you want to make a call to fetch the productDetails for the main product and another to call to fetch the recommendedProducts to show some alternatives and you naively fetch them like so:

const ProductPage = async ({productId}: Props) => {
  const productDetails = (await getProductDetails(productId)) ?? {};
  const recommendedProducts = (await getRecommendedProducts(productId)) ?? [];
  return (<>/*render page*/</>)
}

This is un-optimal as you're fetching the data in series meaning that the time taken to fetch all of the data is the total sum that each request takes

total time = request 1 + request 2

Since each fetch doesn't rely on each other and only relies on the productId, they can actually be called concurrently which makes the total time required to fetch all of the data just the time it took for whichever request took the longest

total time = max(request 1, request 2)

We can make the requests concurrently by using Promise.all:

const [productDetails, recommendedProducts] = await Promise.all([
  getProductDetails(productId),
  getRecommendedProducts(productId)
]);

However this optimisation introduces a subtle change in behaviour when one of the requests fail, if either getProductDetails or getRecommendedProducts fail then Promise.all will fail in turn. This means that if you still want to render the recommended products even if the product details request fails (or vice versa), this approach doesn't currently support it as everything is rejected by Promise.all

Here's a code snippet that you can chuck into dev tools to see what I mean:

const failedFunction = async () => {
     let result;
     try {
         result = await Promise.all([Promise.reject('one'), Promise.resolve('two')])
     } catch (e) {
         console.log('caught error: ', e)
     }
     console.log('result: ', result)
}
await failedFunction()

A simple workaround for this is to use Promise.allSettled instead. With this, the result of both requests are saved and the failure of one no longer prevents the other request from resolving.

const halfFailedFunction = async () => {
     let result;
     try {
         result = await Promise.allSettled([Promise.reject('one'), Promise.resolve('two')])
     } catch (e) {
         console.log('caught error: ', e)
     }
     console.log('result: ', result)
}
await halfFailedFunction()