Handling Multiple API Requests with Promise.all and Promise.allSettled
James Reed
Infrastructure Engineer · Leapcell

Introduction
In modern web development, it's commonplace for applications to interact with various backend services, often requiring multiple API calls to fetch data, update resources, or perform complex operations. When dealing with a batch of such API requests, efficient and robust handling becomes paramount. Two powerful JavaScript Promise combinators, Promise.all
and Promise.allSettled
, offer distinct approaches to managing these concurrent operations. Understanding their nuances is crucial for developers to choose the right tool for the job, ensuring application stability and an optimal user experience. This article delves into the practical applications of these methods, exploring their core functionalities, differences, and guiding you on when to use each for effective batch API request processing.
Core Concepts and Practical Applications
Before diving into the specifics, let's establish a common understanding of the core concepts involved in asymmetrical operations in JavaScript, particularly Promises.
Promises: A Promise is an object representing the eventual completion or failure of an asynchronous operation. It has three states:
- Pending: Initial state, neither fulfilled nor rejected.
- Fulfilled: Meaning that the operation completed successfully.
- Rejected: Meaning that the operation failed.
Now, let's explore Promise.all
and Promise.allSettled
.
Promise.all Explained
Promise.all
takes an iterable of Promises (e.g., an array of Promises) and returns a single Promise. This returned Promise resolves when all of the input Promises have resolved, resolving with an array of their resolved values, in the same order as the input Promises. It rejects as soon as any of the input Promises reject, with the reason of the first rejected Promise.
Principle: "All or nothing." If even one request fails, the entire batch operation is considered a failure. This is ideal when the success of all requests is interdependent.
Use Case:
Imagine an e-commerce application where you need to fetch product details and user reviews simultaneously before displaying a product page. If either of these API calls fails, displaying an incomplete product page would be unhelpful or even misleading. In this scenario, you'd want Promise.all
to ensure both pieces of data are successfully retrieved.
Implementation Example:
// Simulate API calls const fetchProductDetails = (productId) => { return new Promise((resolve, reject) => { setTimeout(() => { if (productId === 'p123') { resolve({ id: 'p123', name: 'Premium Widget', price: 29.99 }); } else { reject(new Error(`Product ${productId} not found`)); } }, 1000); }); }; const fetchUserReviews = (productId) => { return new Promise((resolve, reject) => { setTimeout(() => { if (productId === 'p123') { resolve([ { user: 'Alice', rating: 5, comment: 'Great product!' }, { user: 'Bob', rating: 4, comment: 'Good value.' } ]); } else if (productId === 'p456') { // Simulate a failing review request reject(new Error('Failed to fetch reviews for product p456')); } else { resolve([]); // No reviews for other products } }, 800); }); }; const productId = 'p123'; // Example: Successful scenario // const productId = 'p456'; // Example: One request fails Promise.all([ fetchProductDetails(productId), fetchUserReviews(productId) ]) .then(([productDetails, userReviews]) => { console.log("Promise.all Success:"); console.log("Product Details:", productDetails); console.log("User Reviews:", userReviews); // Render product page with both pieces of data }) .catch(error => { console.error("Promise.all Failed:", error.message); // Display an error message to the user or retry }); // Example with a failing request (uncomment to test) /* const failingProductId = 'p456'; Promise.all([ fetchProductDetails(failingProductId), // This might succeed fetchUserReviews(failingProductId) // This will reject ]) .then(([productDetails, userReviews]) => { console.log("Promise.all Success (should not reach here):", productDetails, userReviews); }) .catch(error => { console.error("Promise.all with failure:", error.message); // Output: Failed to fetch reviews for product p456 // The entire batch failed because one request failed }); */
Promise.allSettled Explained
Promise.allSettled
also takes an iterable of Promises and returns a single Promise. This returned Promise resolves when all of the input Promises have either fulfilled or rejected. It resolves with an array of objects, each describing the outcome of a corresponding Promise. Each object will have a status
property (either 'fulfilled'
or 'rejected'
) and either a value
(if fulfilled) or a reason
(if rejected).
Principle: "Get all outcomes, regardless of individual success or failure." This is useful when you want to know the result of every operation in the batch, even if some of them fail, and you can proceed with the successful ones.
Use Case: Consider a social media application where a user wants to upload multiple images simultaneously. Some uploads might succeed, others might fail due to network issues or invalid file types. The application should inform the user about the status of each upload (e.g., "Image 1 uploaded," "Image 2 failed"), rather than failing the entire operation just because one image couldn't be processed.
Implementation Example:
// Simulate API calls const uploadImage = (fileName, artificialFailure = false) => { return new Promise((resolve, reject) => { setTimeout(() => { if (artificialFailure || fileName.includes('failed')) { reject(new Error(`Failed to upload ${fileName}`)); } else { resolve({ fileName, url: `https://example.com/images/${fileName}` }); } }, Math.random() * 1000 + 500); // Simulate variable upload times }); }; const imagesToUpload = [ 'photo1.jpg', 'avatar.png', 'corrupted_image.failed.gif', // This one will fail 'document.pdf' ]; const uploadPromises = imagesToUpload.map(img => uploadImage(img)); Promise.allSettled(uploadPromises) .then(results => { console.log("Promise.allSettled Results:"); const successfulUploads = []; const failedUploads = []; results.forEach((result, index) => { if (result.status === 'fulfilled') { console.log(`Image ${imagesToUpload[index]} uploaded successfully:`, result.value.url); successfulUploads.push(result.value); } else { console.error(`Image ${imagesToUpload[index]} failed to upload:`, result.reason.message); failedUploads.push({ fileName: imagesToUpload[index], reason: result.reason.message }); } }); console.log("\nSummary:"); console.log("Successfully uploaded:", successfulUploads); console.log("Failed uploads:", failedUploads); // Update UI based on individual outcomes, e.g., show progress for each }) .catch(error => { // This catch block will typically not be reached by Promise.allSettled // unless the input iterable itself is invalid or some synchronous error occurs. console.error("Unexpected error with Promise.allSettled:", error); });
Choosing Between Promise.all and Promise.allSettled
The decision largely hinges on the desired behavior when one or more of your API requests fail:
-
Use
Promise.all
when:- All operations are critical and dependent on each other. If one fails, the entire transaction should be rolled back or considered incomplete.
- You only care about the collective success of all requests.
- Failing fast is an acceptable or desired behavior.
-
Use
Promise.allSettled
when:- You need to process results for all requests, regardless of individual success or failure.
- The operations are largely independent, and the failure of one does not necessitate the failure of the entire batch.
- You want to provide detailed feedback to the user about which specific operations succeeded and which failed.
- You want to gracefully handle errors while still processing successful outcomes.
Conclusion
Both Promise.all
and Promise.allSettled
are invaluable tools for orchestrating multiple asynchronous operations, particularly in the context of batch API requests. Promise.all
enforces an "all or nothing" contract, suitable for interdependent tasks where collective success is paramount. Conversely, Promise.allSettled
offers a more resilient approach, allowing you to ascertain the outcome of every request, making it ideal for scenarios where partial success is acceptable and individual status reporting is crucial. By understanding their distinct behaviors and applying them appropriately, developers can build more robust, fault-tolerant, and user-friendly JavaScript applications. Choose Promise.all
for critical, interdependent operations and Promise.allSettled
for resilient processing where individual outcomes matter.