Understanding Promises in Modern JavaScript
Promises have become the backbone of asynchronous JavaScript, providing a cleaner alternative to callback-based patterns. With the widespread adoption of async/await syntax, understanding how Promises work under the hood remains crucial for every JavaScript developer.
In Next.js applications, you'll frequently encounter scenarios where multiple async operations need coordination--fetching several API endpoints simultaneously, loading data for server-side rendering, or handling concurrent user requests. For teams building modern web applications, mastering Promise methods is essential for optimal performance.
The JavaScript runtime provides several static methods on the Promise constructor that help you coordinate multiple promises effectively: Promise.all(), Promise.allSettled(), Promise.race(), and Promise.then(). Each serves distinct purposes and understanding when to use each one is key to writing efficient, maintainable async code.
Quick Reference: When to Use Each Method
| Method | Use When | Behavior |
|---|---|---|
Promise.all() | All operations MUST succeed | Rejects if ANY promise rejects |
Promise.allSettled() | Operations are independent | Waits for ALL to settle |
Promise.race() | You need the FIRST result | Resolves with first settled promise |
Promise.then() | Chaining operations | Returns new promise for chaining |
Promise.all(): Execute Operations in Parallel
What Promise.all() Does
Promise.all() takes an iterable of promises as input and returns a single Promise that fulfills when all of the input's promises fulfill. The returned promise resolves to an array containing all the fulfillment values, in the same order as the input promises.
This method is particularly useful when you have multiple independent async operations that must all complete successfully before proceeding. However, Promise.all() rejects immediately when ANY of the input promises rejects--this fail-fast behavior makes it ideal for scenarios where all operations are critical.
// Fetching multiple API endpoints in parallel
async function fetchPageData() {
const [userData, productData, settingsData] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/products').then(r => r.json()),
fetch('/api/settings').then(r => r.json())
]);
return { userData, productData, settingsData };
}
Error Handling with Promise.all()
When any promise in the array rejects, Promise.all() immediately rejects with that first rejection reason. You won't get results from the other promises that might have succeeded.
Promise.allSettled(): Handle All Results Independently
Introduction to Promise.allSettled()
Promise.allSettled() waits for all promises in the iterable to settle--meaning they either fulfill or reject--before the returned promise fulfills. Unlike Promise.all(), it doesn't reject when individual promises fail.
This method was introduced to JavaScript in 2020 for scenarios where you need results from all operations, regardless of individual outcomes. It's valuable when making several API calls that don't depend on each other, especially when building full-stack JavaScript applications that aggregate data from multiple sources.
Return Value Structure
// Each outcome object has:
{
status: 'fulfilled' | 'rejected', // Always present
value: any, // Present if fulfilled
reason: any // Present if rejected
}
async function loadOptionalFeatures() {
const results = await Promise.allSettled([
fetch('/api/premium-features').then(r => r.json()),
fetch('/api/recommendations').then(r => r.json()),
fetch('/api/social-data').then(r => r.json())
]);
const features = results[0].status === 'fulfilled' ? results[0].value : null;
const recommendations = results[1].status === 'fulfilled' ? results[1].value : [];
const socialData = results[2].status === 'fulfilled' ? results[2].value : {};
return { features, recommendations, socialData };
}
When to Prefer allSettled() Over all()
- Operations are independent and can succeed or fail separately
- You want to implement graceful fallback or retry logic
- You're loading optional features that shouldn't block functionality
- You need to collect all errors for logging
Promise.race(): Get the First Result
Understanding Promise.race()
Promise.race() returns a Promise that settles as soon as any of the input promises settles. The returned promise adopts the state and value/reason of that first settled promise.
Practical Use Case: Request Timeouts
One common application is implementing request timeouts:
function fetchWithTimeout(url, timeoutMs = 5000) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timeout')), timeoutMs);
});
return Promise.race([
fetch(url).then(r => r.json()),
timeoutPromise
]);
}
Implementing Fallbacks with Promise.race()
async function getUserData() {
const freshData = fetch('/api/user').then(r => ({ source: 'network', data: r.json() }));
const cachedData = caches.match('/api/user').then(r => r ? ({ source: 'cache', data: r.json() }) : null);
const result = await Promise.race([freshData, cachedData]);
return result.data;
}
Promise.then(): The Foundation of Promise Chains
Chaining and Composition
Promise.then() is the most fundamental Promise method, allowing you to attach fulfillment and rejection handlers. It returns a new promise, enabling powerful chaining patterns.
fetch('/api/data')
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => {
console.log('Data loaded:', data);
return processData(data);
})
.catch(error => {
console.error('Error in chain:', error);
});
Separate Error Handling
fetch('/api/user')
.then(
user => validateUser(user),
error => handleNetworkError(error)
)
.then(
validatedUser => saveUser(validatedUser),
error => handleValidationError(error)
);
Connection to async/await
Under the hood, async/await is syntactic sugar over Promise.then() chains:
// This async/await code:
const data = await fetchData();
processData(data);
// Desugars to this:
fetchData().then(data => processData(data));
Performance Considerations for Next.js Applications
Parallel vs Sequential Execution
All static Promise methods execute input promises in parallel:
// SLOW: Sequential
const slow = await fetch1();
const slower = await fetch2();
// FAST: Parallel
const [fast, faster] = await Promise.all([fetch1(), fetch2()]);
Server-Side Rendering Considerations
- Use
Promise.all()for critical data required in initial render - Use
Promise.allSettled()for non-blocking data - Implement timeouts with
Promise.race()for API calls
When building full-stack JavaScript applications, proper use of these Promise methods can significantly impact performance and user experience. For applications requiring AI-powered automation, efficient async patterns are critical for handling multiple API calls to external services.
Conclusion
Mastering JavaScript's Promise concurrency methods is essential for building responsive web applications:
Promise.all()for when you need all operations to succeed togetherPromise.allSettled()for independent operations where you want all resultsPromise.race()for timeouts and getting the first resultPromise.then()for chaining and granular error handling
Choose the right method based on your requirements for success/failure handling and user experience.
Choose the right Promise method for your use case
Promise.all()
Use when ALL operations must succeed. Rejects immediately if any promise fails.
Promise.allSettled()
Use for independent operations. Returns results from all promises regardless of success or failure.
Promise.race()
Use for timeouts or when you need the first result. Resolves with whichever promise settles first.
Promise.then()
The foundation for chaining. Allows separate handling of success and failure cases.
const [users, products, orders] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/products').then(r => r.json()),
fetch('/api/orders').then(r => r.json())
]);
// All succeed or reject together
Frequently Asked Questions
What is the difference between Promise.all() and Promise.allSettled()?
Promise.all() rejects immediately if any promise rejects, while Promise.allSettled() waits for all promises to settle (fulfill or reject) and returns an array of outcome objects. Use allSettled() when you need results from all operations regardless of individual success or failure.
When should I use Promise.race()?
Use Promise.race() for implementing request timeouts, falling back to cached data, or when you only need the first operation to complete. It's commonly used to prevent hanging indefinitely on slow network requests.
Does Promise.all() run promises in parallel?
Yes, Promise.all() executes all input promises concurrently, not sequentially. However, the promises must be created before being passed to Promise.all()--if you create them inside the array, they start executing immediately.
How does async/await relate to Promise methods?
async/await is syntactic sugar over Promises. When you write `await fetchData()`, it's equivalent to `fetchData().then(...)`. Understanding this helps debug async code and work with promise-based APIs.
Sources
- MDN Web Docs - Promise.all() - Official JavaScript documentation for Promise.all() behavior and syntax
- MDN Web Docs - Promise.allSettled() - Official documentation for Promise.allSettled() including return value structure
- MDN Web Docs - Promise.race() - Official documentation for Promise.race() concurrency method
- MDN Web Docs - Promise.then() - Official documentation for Promise.then() method
- LogRocket Blog - Is Promise.all still relevant in 2025? - Modern perspective on Promise.all usage patterns in JavaScript development