Promises Deep Dive
Promise internals, chaining, error handling, Promise.all, Promise.race, and more
Understanding Promises
A Promise represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Promises provide a cleaner alternative to callbacks for handling async code.
Promise States
- Pending — Initial state, neither fulfilled nor rejected
- Fulfilled — Operation completed successfully
- Rejected — Operation failed
- Settled — Either fulfilled or rejected (final state)
Creating Promises
// Basic Promise creation
const promise = new Promise((resolve, reject) => {
// Async operation
setTimeout(() => {
const success = true;
if (success) {
resolve("Operation succeeded!"); // Fulfill
} else {
reject(new Error("Operation failed")); // Reject
}
}, 1000);
});
// Consuming the promise
promise
.then(result => console.log(result)) // "Operation succeeded!"
.catch(error => console.error(error));
// Shorthand for resolved/rejected promises
Promise.resolve("immediate value");
Promise.reject(new Error("immediate error"));
Promise Chaining
.then() always returns a new Promise, enabling chaining:
fetch("/api/user")
.then(response => response.json()) // Returns Promise
.then(user => fetch(`/api/posts/${user.id}`)) // Returns Promise
.then(response => response.json())
.then(posts => {
console.log("Posts:", posts);
return posts.length;
})
.then(count => console.log(`Found ${count} posts`))
.catch(error => console.error("Error:", error));
// Each .then() receives the value from the previous one
// Returning a Promise waits for it to resolve
Error Handling
// .catch() handles errors from any point in the chain
fetchUser()
.then(user => fetchPosts(user.id))
.then(posts => displayPosts(posts))
.catch(error => {
// Catches errors from fetchUser, fetchPosts, or displayPosts
console.error("Something went wrong:", error);
});
// Multiple catch blocks for specific handling
fetchUser()
.then(user => {
if (!user.isActive) {
throw new Error("User inactive");
}
return fetchPosts(user.id);
})
.catch(error => {
if (error.message === "User inactive") {
return []; // Return empty array, chain continues
}
throw error; // Re-throw other errors
})
.then(posts => console.log(posts));
// .finally() runs regardless of success/failure
fetchData()
.then(data => process(data))
.catch(error => handleError(error))
.finally(() => {
hideLoadingSpinner(); // Always runs
});
Promise.all()
Wait for multiple promises to resolve. Rejects if any promise rejects:
const promise1 = fetch("/api/users");
const promise2 = fetch("/api/posts");
const promise3 = fetch("/api/comments");
Promise.all([promise1, promise2, promise3])
.then(([users, posts, comments]) => {
// All three resolved
console.log("Users:", users);
console.log("Posts:", posts);
console.log("Comments:", comments);
})
.catch(error => {
// First rejection fails the whole thing
console.error("One request failed:", error);
});
// Practical example: Parallel API calls
async function loadDashboard() {
const [user, notifications, stats] = await Promise.all([
fetchUser(),
fetchNotifications(),
fetchStats()
]);
return { user, notifications, stats };
}
Promise.allSettled()
Wait for all promises to settle, regardless of success/failure:
const promises = [
Promise.resolve("Success"),
Promise.reject("Error"),
Promise.resolve("Another success")
];
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`Promise ${index}: ${result.value}`);
} else {
console.log(`Promise ${index} failed: ${result.reason}`);
}
});
});
// Output:
// Promise 0: Success
// Promise 1 failed: Error
// Promise 2: Another success
Promise.race()
Returns when the first promise settles (resolves or rejects):
// Timeout pattern
function fetchWithTimeout(url, timeout = 5000) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Timeout")), timeout);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
fetchWithTimeout("/api/slow-endpoint", 3000)
.then(response => response.json())
.catch(error => console.error(error)); // "Timeout" if > 3s
Promise.any()
Returns when the first promise fulfills. Only rejects if all reject:
// Try multiple sources, use first success
const mirrors = [
fetch("https://mirror1.com/data"),
fetch("https://mirror2.com/data"),
fetch("https://mirror3.com/data")
];
Promise.any(mirrors)
.then(response => {
console.log("Got data from fastest mirror");
return response.json();
})
.catch(error => {
// AggregateError if ALL mirrors failed
console.error("All mirrors failed:", error.errors);
});
Creating Promise Utilities
// Delay function
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
await delay(1000); // Wait 1 second
// Retry with exponential backoff
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await fetch(url);
} catch (error) {
if (i === retries - 1) throw error;
await delay(Math.pow(2, i) * 1000); // 1s, 2s, 4s
}
}
}
// Sequential execution
async function sequential(tasks) {
const results = [];
for (const task of tasks) {
results.push(await task());
}
return results;
}
Common Patterns
// Promisify callback-based function
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (error, result) => {
if (error) reject(error);
else resolve(result);
});
});
};
}
const readFileAsync = promisify(fs.readFile);
const data = await readFileAsync("file.txt", "utf8");
// Memoized promise (cache result)
function memoizedFetch(url) {
const cache = new Map();
return function() {
if (!cache.has(url)) {
cache.set(url, fetch(url).then(r => r.json()));
}
return cache.get(url);
};
}
⚠️ Common Mistakes
- Forgetting to return: Always return promises in .then() chains
- Nesting instead of chaining: Avoid callback hell in promises
- Unhandled rejections: Always have a .catch() or try/catch
- Creating unnecessary promises: Don't wrap fetch() in new Promise()
📚 Learn More
💡 Key Takeaways
- • Promises represent eventual values and have three states
- • Chain with .then(), handle errors with .catch(), cleanup with .finally()
- • Use Promise.all() for parallel operations, Promise.race() for timeouts
- • Promise.allSettled() when you need all results regardless of failures
- • Always handle rejections to avoid unhandled promise rejection warnings