Written by Rahul · Frontend Engineer at Google · Updated 2025
The 5-Year-Old Explanation
Imagine you go to a pizza shop. You order a pizza. The shop gives you a receipt (that's the Promise). The pizza isn't ready yet, but the receipt promises you'll get it.
Three things can happen:
- 🍕 Fulfilled — Your pizza is ready! (
.then()) - ❌ Rejected — They're out of cheese. Sorry! (
.catch()) - ⏳ Pending — Still in the oven...
You don't stand at the counter waiting. You go sit down (the code keeps running), and they'll call your name when it's done (the callback runs).
The Real Technical Version
// Creating a Promise
const pizzaOrder = new Promise((resolve, reject) => {
const hasCheese = true;
setTimeout(() => {
if (hasCheese) {
resolve({ type: "Margherita", size: "Large" });
} else {
reject(new Error("Out of cheese!"));
}
}, 2000);
});
// Consuming the Promise
pizzaOrder
.then(pizza => console.log("Got pizza:", pizza))
.catch(error => console.error("Order failed:", error))
.finally(() => console.log("Transaction complete"));Promise Chaining — The Power Feature
fetch("/api/user/123")
.then(response => {
if (!response.ok) throw new Error("User not found");
return response.json(); // Returns a new Promise
})
.then(user => {
return fetch(`/api/posts?userId=${user.id}`); // Chain another fetch
})
.then(response => response.json())
.then(posts => {
console.log("User posts:", posts);
})
.catch(error => {
// Catches ANY error in the chain
console.error("Something went wrong:", error);
});Promise Static Methods — The Toolkit
Promise.all — All or Nothing
// Fetch multiple resources in parallel
const [users, posts, comments] = await Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/posts").then(r => r.json()),
fetch("/api/comments").then(r => r.json()),
]);
// If ANY one fails, the whole thing rejects
// Use case: Dashboard that needs all data to renderPromise.allSettled — Get All Results
const results = await Promise.allSettled([
fetch("/api/critical"),
fetch("/api/optional"),
fetch("/api/nice-to-have"),
]);
results.forEach(result => {
if (result.status === "fulfilled") {
console.log("Success:", result.value);
} else {
console.log("Failed:", result.reason);
}
});
// Use case: When some failures are acceptablePromise.race — First One Wins
// Implement a timeout for fetch
const fetchWithTimeout = (url, timeout = 5000) => {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Request timed out")), timeout)
)
]);
};
// Use case: Race between fetch and timeout
const data = await fetchWithTimeout("/api/slow-endpoint", 3000);Real Production Patterns
Pattern 1: Retry with Exponential Backoff
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (response.ok) return await response.json();
throw new Error(`HTTP ${response.status}`);
} catch (error) {
if (i === maxRetries - 1) throw error;
const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s
console.log(`Retry ${i + 1} in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}Pattern 2: Sequential Promise Execution
// Process items one at a time (not in parallel)
async function processSequentially(items) {
const results = [];
for (const item of items) {
const result = await processItem(item); // Wait for each one
results.push(result);
}
return results;
}
// Process with concurrency limit
async function processWithLimit(items, limit = 3) {
const results = [];
const executing = new Set();
for (const item of items) {
const promise = processItem(item).then(result => {
executing.delete(promise);
return result;
});
executing.add(promise);
results.push(promise);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}Production Issues
Issue 1: Unhandled Promise Rejections
// ❌ This silently fails in production
fetch("/api/data").then(r => r.json());
// ✅ Always handle errors
fetch("/api/data")
.then(r => r.json())
.catch(err => showErrorToUser(err));
// Node.js will crash on unhandled rejections (since Node 15)
process.on("unhandledRejection", (reason) => {
console.error("Unhandled:", reason);
process.exit(1);
});Issue 2: Promise vs Callback Confusion
// ❌ Mixing callbacks and promises
function getData(callback) {
fetch("/api")
.then(r => r.json())
.then(data => callback(null, data))
.catch(err => callback(err));
}
// ✅ Just return the promise
async function getData() {
const response = await fetch("/api");
return response.json();
}Best Practices
- Always catch errors — every Promise chain needs a
.catch()ortry/catch - Use
async/awaitfor readability — but understand Promises underneath - Use
Promise.allfor parallel operations — don'tawaitsequentially when you can parallelize - Use
Promise.allSettledwhen partial failure is acceptable - Add timeouts to all network requests with
Promise.race