Better error handling when trying to optimize sequential promises
Have you seen a code like this before?
1const currentUser = await fetchCurrentUserInfo();2const blogPosts = await fetchBlogPosts();
And felt that this code could be optimized given these requests are independent and running them at the same time wouldn't cause any harm ?
Therefor the best way to make them run concurrently without waiting each others to resolve is Promise.all():
1const [currentUser, blogPosts] = await Promise.all([2 fetchCurrentUserInfo(),3 fetchBlogPosts()4])
But there still a problem with this , We're not handling errors 🤔, Sure lets put a big try-catch block here:
1try {2 const [currentUser, blogPosts] = await Promise.all([3 fetchCurrentUserInfo(),4 fetchBlogPosts()5 ])6} catch (err) {7 console.log(err)8}
Till now we're better, but there still a major issue which is the error handling in this cases!
Q: What if fetchCurrentUserInfo() got rejected ?
A: It will throw an unhandled promise and cause the second promise results to disappear
Proof: Let's mock some implementations for both function to test it as in a real life scenario and detect what's the issue is:
1async function fetchCurrentUserInfo() {2 return new Promise((resolve, reject) => {3 setTimeout(() => {4 resolve('Resolved user info after 1 second')5 }, 1000)6 })7}89async function fetchBlogPosts() {10 return new Promise((resolve, reject) => {11 setTimeout(() => {12 resolve('Resolved blog posts after 2 seconds')13 }, 2000)14 })15}1617try {18 const [currentUser, blogPosts] = await Promise.all([19 fetchCurrentUserInfo(),20 fetchBlogPosts(),21 ])22 console.log({ currentUser, blogPosts })23} catch (err) {24 console.log(err)25}
Result will be:
1{2 currentUser: 'Resolved user info after 1 second',3 blogPosts: 'Resolved blog posts after 2 seconds'4}
Now let's reject one of the promises and further will visualize their outputs:
1async function fetchCurrentUserInfo() {2 return new Promise((resolve, reject) => {3 setTimeout(() => {4 reject('Rejected fetchCurrentUserInfo() after 1 second')5 }, 1000)6 })7}89async function fetchBlogPosts() {10 return new Promise((resolve, reject) => {11 setTimeout(() => {12 resolve('Resolved blog posts after 2 seconds')13 }, 2000)14 })15}161718try {19 const [currentUser, blogPosts] = await Promise.all([20 fetchCurrentUserInfo(),21 fetchBlogPosts(),22 ])23 console.log({ currentUser, blogPosts })24} catch (err) {25 console.log(err)26}
Result for this will be:
1Rejected fetchCurrentUserInfo() after 1 second
See the issue here ? The first promise rejected and threw an unhandled promise and caused the second one's result to disappear, this can be good in some cases where the second promise depends on the first's data so it will optimize the request time could've been wasted in running the second promise that we know will throw an error as well.
But in our case we wan't the second one to run as it may succeed and serves the blog posts to the user until we handle the first error and retry getting userInfo only.
Another scenario to imagine is if the second promise would've been rejected too, in this case we would never know until the first one is resolved which will waste again more time during debugging or even in production since once we'll fix the first promise well get another error again for the second and go back fix it again when we could've got both errors in the first time and be aware of it at once rather than separately.
The solution this is Promise.allSettled().
Let's rewrite the same logic but using our new function ( Promise.allSettled() ) this time.
1async function fetchCurrentUserInfo() {2 return new Promise((resolve, reject) => {3 setTimeout(() => {4 reject('Rejected fetchCurrentUserInfo() after 1 second')5 }, 1000)6 })7}89async function fetchBlogPosts() {10 return new Promise((resolve, reject) => {11 setTimeout(() => {12 resolve('Resolved blog posts after 2 seconds')13 }, 2000)14 })15}161718try {19 const [currentUser, blogPosts] = await Promise.allSettled([20 fetchCurrentUserInfo(),21 fetchBlogPosts(),22 ])23 console.log({ currentUser, blogPosts })24} catch (err) {25 console.log(err)26}
The result now is:
1{2 "currentUser": {3 "status": "rejected",4 "reason": "Rejected fetchCurrentUserInfo() after 1 second"5 },6 "blogPosts": {7 "status": "fulfilled",8 "value": "Resolved blog posts after 2 seconds"9 }10}
Now a brief description of what is Promise.allSettled(). from what we've got above would be:
Promise.allSettled() is a method in JavaScript that returns a promise that is fulfilled when all of the promises in an iterable have been settled, which means that they have either been fulfilled or rejected.
It returns an array of objects, each representing the outcome of each promise. The outcome of each promise is either "fulfilled" or "rejected", and the value of the promise is represented by the value property if it was fulfilled, or the reason property if it was rejected.
We can now remove the try-catch block because now we get and object back that will tell us if the promise was fulfilled or rejected, and the better version with optimized concurrent promises and error handled would look like this:
1async function fetchCurrentUserInfo() {2 return new Promise((resolve, reject) => {3 setTimeout(() => {4 reject('Rejected fetchCurrentUserInfo() after 1 second')5 }, 1000)6 })7}89async function fetchBlogPosts() {10 return new Promise((resolve, reject) => {11 setTimeout(() => {12 resolve('Resolved blog posts after 2 seconds')13 }, 2000)14 })15}1617const result = await Promise.allSettled([18 fetchCurrentUserInfo(),19 fetchBlogPosts(),20])2122const [currentUser, blogPosts] = handleResponses(result)
handleResponses() function could now process results, handle any errors and if successful return us our userInfo and blogPosts objects 🎉