Promises — Deep Dive
📖 Concept
Promises represent a value that may be available now, later, or never. They solve callback hell by enabling chaining and composition of async operations.
Promise states:
- Pending — Initial state; operation in progress
- Fulfilled — Operation completed successfully (
.then()fires) - Rejected — Operation failed (
.catch()fires)
Once settled (fulfilled or rejected), a Promise cannot change state — it's immutable.
Promise chaining:
Each .then() returns a new Promise, enabling flat chains instead of nested callbacks:
readFile('a.txt')
.then(data => transform(data))
.then(result => writeFile('b.txt', result))
.then(() => console.log('Done!'))
.catch(err => console.error('Failed:', err));
Key combinators:
| Method | Behavior |
|---|---|
Promise.all([p1, p2]) |
Resolves when ALL resolve; rejects on first rejection |
Promise.allSettled([p1, p2]) |
Waits for ALL to settle; never rejects |
Promise.race([p1, p2]) |
Resolves/rejects with the first to settle |
Promise.any([p1, p2]) |
Resolves with the first to fulfill; rejects only if ALL reject |
Common pitfalls:
- Forgetting
.catch()→ unhandled rejection (crashes in Node.js 15+) - Nesting
.then()inside.then()→ recreating callback hell - Not returning inside
.then()→ next.then()receivesundefined - Creating Promises for already-synchronous operations (unnecessary overhead)
🏠 Real-world analogy: A Promise is like an order receipt at a fast-food restaurant. You get the receipt immediately (Promise), and eventually it's either fulfilled (food ready) or rejected (sold out). You can chain actions: "when food is ready → add ketchup → sit down → eat."
💻 Code Example
1// Promises — Deep Dive23const fs = require("fs").promises;4const path = require("path");56// 1. Creating Promises7function delay(ms) {8 return new Promise((resolve) => setTimeout(resolve, ms));9}1011function fetchUser(id) {12 return new Promise((resolve, reject) => {13 setTimeout(() => {14 if (id <= 0) reject(new Error("Invalid user ID"));15 else resolve({ id, name: `User_${id}`, email: `user${id}@example.com` });16 }, 100);17 });18}1920// 2. Promise chaining — flat and readable21fetchUser(1)22 .then((user) => {23 console.log("User:", user.name);24 return fetchOrders(user.id); // Return a Promise for chaining25 })26 .then((orders) => {27 console.log("Orders:", orders.length);28 return orders[0]; // Return a value (auto-wrapped in Promise)29 })30 .then((firstOrder) => {31 console.log("First order:", firstOrder);32 })33 .catch((err) => {34 console.error("Error in chain:", err.message);35 })36 .finally(() => {37 console.log("Chain complete (runs regardless of success/failure)");38 });3940// 3. Promise.all — Parallel execution41async function loadDashboardData() {42 const startTime = Date.now();4344 // ✅ GOOD: All requests run in parallel45 const [user, orders, notifications] = await Promise.all([46 fetchUser(1),47 fetchOrders(1),48 fetchNotifications(1),49 ]);5051 console.log(`Dashboard loaded in ${Date.now() - startTime}ms`);52 // ~100ms (parallel) instead of ~300ms (sequential)53 return { user, orders, notifications };54}5556// 4. Promise.allSettled — Get all results even if some fail57async function loadDataWithGracefulFailure() {58 const results = await Promise.allSettled([59 fetchUser(1),60 fetchUser(-1), // This will reject61 fetchUser(3),62 ]);6364 results.forEach((result, i) => {65 if (result.status === "fulfilled") {66 console.log(`Request ${i}: Success —`, result.value.name);67 } else {68 console.log(`Request ${i}: Failed —`, result.reason.message);69 }70 });7172 // Extract only successful results73 const successfulUsers = results74 .filter((r) => r.status === "fulfilled")75 .map((r) => r.value);76 console.log("Successful users:", successfulUsers.length);77}7879// 5. Promise.race — First to settle wins80async function fetchWithTimeout(promise, timeoutMs) {81 const timeout = new Promise((_, reject) =>82 setTimeout(() => reject(new Error("Timeout")), timeoutMs)83 );84 return Promise.race([promise, timeout]);85}8687// Usage88fetchWithTimeout(fetchUser(1), 50)89 .then((user) => console.log("Got user before timeout:", user.name))90 .catch((err) => console.log("Timed out:", err.message));9192// 6. Promise.any — First to fulfill (ignores rejections)93async function fetchFromFastestMirror() {94 try {95 const data = await Promise.any([96 fetchFromMirror("us-east"),97 fetchFromMirror("eu-west"),98 fetchFromMirror("ap-south"),99 ]);100 console.log("Fastest mirror:", data);101 } catch (err) {102 // AggregateError — ALL mirrors failed103 console.error("All mirrors failed:", err.errors);104 }105}106107// 7. Error handling patterns108// ❌ BAD: Missing .catch()109// fetchUser(1).then(user => console.log(user));110// If fetchUser rejects → UnhandledPromiseRejection → process crash (Node 15+)111112// ✅ GOOD: Always handle errors113fetchUser(1)114 .then((user) => console.log(user))115 .catch((err) => console.error(err));116117// ✅ BEST: Centralized error handling in chains118function processOrder(userId) {119 return fetchUser(userId)120 .then((user) => validateUser(user))121 .then((user) => createOrder(user))122 .then((order) => sendConfirmation(order))123 .catch((err) => {124 // Single catch for the entire chain125 console.error("Order processing failed:", err.message);126 throw err; // Re-throw if caller should handle it too127 });128}129130// 8. Creating pre-resolved/rejected Promises131const resolved = Promise.resolve(42);132const rejected = Promise.reject(new Error("boom"));133rejected.catch(() => {}); // Handle to avoid warning134135// 9. Promisifying callback APIs136const { promisify } = require("util");137const execAsync = promisify(require("child_process").exec);138139// Helper mock functions140function fetchOrders(userId) {141 return new Promise((resolve) =>142 setTimeout(() => resolve([{ id: 1 }, { id: 2 }]), 100)143 );144}145function fetchNotifications(userId) {146 return new Promise((resolve) =>147 setTimeout(() => resolve([{ msg: "Hello" }]), 100)148 );149}150function fetchFromMirror(region) {151 return new Promise((resolve) =>152 setTimeout(() => resolve(region), Math.random() * 200)153 );154}155function validateUser(user) { return Promise.resolve(user); }156function createOrder(user) { return Promise.resolve({ id: 1, user }); }157function sendConfirmation(order) { return Promise.resolve(order); }
🏋️ Practice Exercise
Exercises:
- Implement a
retry(fn, maxAttempts)function using Promises that retries a failing async operation - Use
Promise.all()to fetch data from 5 different sources in parallel and merge the results - Use
Promise.allSettled()to build a health-check that tests 5 services and reports which are up/down - Implement a
timeout(promise, ms)wrapper usingPromise.race() - Build a basic Promise-based task queue that limits concurrency (max 3 simultaneous operations)
- Convert a callback-based library function to Promise-based using both manual wrapping and
util.promisify
⚠️ Common Mistakes
Forgetting to return inside
.then()— the next.then()receivesundefinedinstead of the intended valueNesting
.then()inside.then()— this recreates callback hell; always return Promises for flat chainingNot adding
.catch()at the end of a Promise chain — unhandled rejections crash the Node.js process in v15+Using
Promise.all()when partial failures are acceptable — usePromise.allSettled()instead to get all resultsCreating unnecessary Promises with
new Promise()around already async code — if a function returns a Promise, just return it directly
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Promises — Deep Dive