Improving Async Code with Promise.allSettled() in JavaScript

Published on | Reading time: 6 min | Author: Andrés Reyes Galgani

Improving Async Code with Promise.allSettled() in JavaScript
Photo courtesy of Dayne Topkin

Table of Contents

  1. Introduction
  2. Problem Explanation
  3. Solution with Code Snippet
  4. Practical Application
  5. Potential Drawbacks and Considerations
  6. Conclusion
  7. Final Thoughts
  8. Further Reading

Introduction

As developers, we often find ourselves wrestling with performance issues, especially when building dynamic applications that handle large datasets or rely heavily on external APIs. During development, there’s a fine line between keeping our code maintainable and pushing for optimal performance, and it can get messy (like a last-minute spreadsheet before a board meeting). 🤖

One powerful tool in our arsenal is the asynchronous processing of API requests and database queries. But how do you maintain clean, manageable code while ensuring peak performance? That's where the Promise.allSettled() method comes in—an often-overlooked feature of JavaScript that can help you improve both the performance and reliability of your code.

In this post, we will explore how to effectively use Promise.allSettled() by examining a practical scenario, demonstrating its capabilities, and showcasing how it can simplify the management of multiple asynchronous operations without sacrificing code quality.


Problem Explanation

In a typical web application, making multiple asynchronous requests to various endpoints or databases is common. One might first think of using Promise.all() for this task, which executes multiple promises in parallel. However, it comes with a catch: if any of the executed promises reject, the entire chain is rejected, and you lose access to results from the other promises.

Consider the following scenario:

const fetchDataFromAPIs = async () => {
    const userPromise = fetch('https://api.example.com/users/1');
    const postsPromise = fetch('https://api.example.com/users/1/posts');
    const commentsPromise = fetch('https://api.example.com/comments');

    const results = await Promise.all([userPromise, postsPromise, commentsPromise]);
    return results.map(response => response.json());
};

If any of those API requests fails (for instance, the comments API returns a 404), you'll receive an error, and the whole operation fails, leaving you without any data. You might think to wrap each fetch in a try-catch block or handle promises individually, leading to cumbersome logic and possibly bloated code.


Solution with Code Snippet

Enter Promise.allSettled()! Unlike Promise.all(), this method does not reject if any of the promises are rejected. Instead, it returns an array of objects representing the results of each promise, making it easier to handle scenarios where some requests succeed while others fail. 🎉

Here’s how you can refactor the earlier example:

const fetchDataFromAPIs = async () => {
    // Define the API calls.
    const userPromise = fetch('https://api.example.com/users/1');
    const postsPromise = fetch('https://api.example.com/users/1/posts');
    const commentsPromise = fetch('https://api.example.com/comments');

    // Using Promise.allSettled
    const results = await Promise.allSettled([userPromise, postsPromise, commentsPromise]);

    const data = results.map(result => {
        // Result is an object with status and value/error.
        if (result.status === 'fulfilled') {
            return result.value.json(); // If resolved, parse the result.
        } else {
            console.error(`Error: ${result.reason}`); // Log the reason for failure.
            return null; // Return null or any other value to handle failed requests.
        }
    });

    return data; // Return the array of data
};

Explanation of Improvements

  1. Error Handling: With Promise.allSettled(), even if one or more requests fail, your application can still harvest meaningful results from successful requests without aborting the entire operation. The conditional check (result.status === 'fulfilled') allows you to decide how to handle errors gracefully.

  2. Cleaner Code: The refactored version of the code is more straightforward. You don’t need multiple try-catch blocks or overly convoluted logic to handle the different outcomes of the promises.

  3. Performance: By sending multiple requests in parallel and handling the results in a single consolidated manner, this approach is typically more efficient than handling each API call sequentially.


Practical Application

Let’s visualize how this method can play out in a real-world application. Imagine a social media dashboard that displays user information, their posts, and comments. Each piece of data comes from different endpoints, and users expect quick access to all of it.

Utilizing Promise.allSettled() allows the app to fetch user data, post data, and comments all at once. If the comments API experiences downtime due to an issue, users will still see their profile information and posts without a noticeable delay. Your UI can notify users about the missing comments information while still being responsive.

Integration in Existing Projects

The above method can seamlessly integrate into existing JavaScript frameworks such as React or Vue. For instance, in a React component, you could call fetchDataFromAPIs() inside a useEffect, ensuring that users experience immediate feedback even in the face of API hiccups.

Here’s how it might look:

import { useEffect, useState } from 'react';

const UserDashboard = () => {
    const [data, setData] = useState({ user: null, posts: null, comments: null });

    useEffect(() => {
        const fetchData = async () => {
            const results = await fetchDataFromAPIs();
            setData({ user: results[0], posts: results[1], comments: results[2] });
        };
        
        fetchData();
    }, []);

    return (
        <div>
            {/* Render user data, posts, and handle potential missing comments */}
        </div>
    );
};

Potential Drawbacks and Considerations

Despite its benefits, Promise.allSettled() is not a catch-all solution. For instance, if your application requires all operations to succeed before proceeding (for state consistency), then using Promise.all() may still be necessary.

Additionally, remember that while Promise.allSettled() handles errors gracefully, it’s vital to ensure that the consequences of failures in specific use cases are well managed. Always assess the specific context of your application and the data requirements for each promise.


Conclusion

Using Promise.allSettled() enhances your asynchronous JavaScript code by enabling better error management and more organized flow without compromising performance or readability. As a developer, this approach empowers you to build resilient applications that can handle the unpredictable nature of API communication—something akin to catching a cat as it jumps from the counter instead of letting it crash to the floor! 🐱

For developers grappling with handling asynchronous data flows, employing this strategy means cleaner code and happier users.


Final Thoughts

Experimenting with Promise.allSettled() might just revolutionize the way you handle multiple asynchronous operations. I encourage you to share your thoughts and experiences using this method. Have you encountered scenarios where it provided a significant advantage? 🌟

Be sure to subscribe for more tips and tricks that will enhance your development skills and keep your applications efficient and user-friendly!


Further Reading

  1. MDN Web Docs on Promise.allSettled()
  2. Asynchronous JavaScript: Understanding Callbacks, Promises, and Async/Await
  3. Error handling with async/await

Focus Keyword: Promise.allSettled