Adil Haddaoui

/ Home/ Uses

Better error handling when trying to optimize sequential promises

Cover Image for Better error handling when trying to optimize sequential promises
Photo of Adil Haddaoui
Adil Haddaoui

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}
8
9async function fetchBlogPosts() {
10 return new Promise((resolve, reject) => {
11 setTimeout(() => {
12 resolve('Resolved blog posts after 2 seconds')
13 }, 2000)
14 })
15}
16
17try {
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}
8
9async function fetchBlogPosts() {
10 return new Promise((resolve, reject) => {
11 setTimeout(() => {
12 resolve('Resolved blog posts after 2 seconds')
13 }, 2000)
14 })
15}
16
17
18try {
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}
8
9async function fetchBlogPosts() {
10 return new Promise((resolve, reject) => {
11 setTimeout(() => {
12 resolve('Resolved blog posts after 2 seconds')
13 }, 2000)
14 })
15}
16
17
18try {
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}
8
9async function fetchBlogPosts() {
10 return new Promise((resolve, reject) => {
11 setTimeout(() => {
12 resolve('Resolved blog posts after 2 seconds')
13 }, 2000)
14 })
15}
16
17const result = await Promise.allSettled([
18 fetchCurrentUserInfo(),
19 fetchBlogPosts(),
20])
21
22const [currentUser, blogPosts] = handleResponses(result)

handleResponses() function could now process results, handle any errors and if successful return us our userInfo and blogPosts objects 🎉


More to read

React Redefined: Unleashing it's Power with a new Compiler

The React's game-changing update: a brand-new compiler that simplifies coding, making it cleaner and more efficient. This major leap forward promises an easier and more enjoyable development experience. In this post, we're diving into the big changes and what they mean for us developers. It's a thrilling time for the React community, and we can't wait to see where this update takes us.

Photo of Adil Haddaoui
Adil Haddaoui

HTMX: Redefining Simplicity in Web Development

Discover the simplicity of web development with HTMX. Dive into practical examples that showcase HTMX's ability to enhance HTML for dynamic updates, form submissions, and real-time search—effortlessly and with minimal code. Learn how HTMX streamlines development, making it easier to create interactive, maintainable web applications.

Photo of Adil Haddaoui
Adil Haddaoui