Promises — Deep Dive

0/4 in this phase0/48 across the roadmap

📖 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:

  1. Pending — Initial state; operation in progress
  2. Fulfilled — Operation completed successfully (.then() fires)
  3. 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() receives undefined
  • 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

codeTap to expand ⛶
1// Promises — Deep Dive
2
3const fs = require("fs").promises;
4const path = require("path");
5
6// 1. Creating Promises
7function delay(ms) {
8 return new Promise((resolve) => setTimeout(resolve, ms));
9}
10
11function 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}
19
20// 2. Promise chaining — flat and readable
21fetchUser(1)
22 .then((user) => {
23 console.log("User:", user.name);
24 return fetchOrders(user.id); // Return a Promise for chaining
25 })
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 });
39
40// 3. Promise.all — Parallel execution
41async function loadDashboardData() {
42 const startTime = Date.now();
43
44 // ✅ GOOD: All requests run in parallel
45 const [user, orders, notifications] = await Promise.all([
46 fetchUser(1),
47 fetchOrders(1),
48 fetchNotifications(1),
49 ]);
50
51 console.log(`Dashboard loaded in ${Date.now() - startTime}ms`);
52 // ~100ms (parallel) instead of ~300ms (sequential)
53 return { user, orders, notifications };
54}
55
56// 4. Promise.allSettled — Get all results even if some fail
57async function loadDataWithGracefulFailure() {
58 const results = await Promise.allSettled([
59 fetchUser(1),
60 fetchUser(-1), // This will reject
61 fetchUser(3),
62 ]);
63
64 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 });
71
72 // Extract only successful results
73 const successfulUsers = results
74 .filter((r) => r.status === "fulfilled")
75 .map((r) => r.value);
76 console.log("Successful users:", successfulUsers.length);
77}
78
79// 5. Promise.race — First to settle wins
80async 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}
86
87// Usage
88fetchWithTimeout(fetchUser(1), 50)
89 .then((user) => console.log("Got user before timeout:", user.name))
90 .catch((err) => console.log("Timed out:", err.message));
91
92// 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 failed
103 console.error("All mirrors failed:", err.errors);
104 }
105}
106
107// 7. Error handling patterns
108// ❌ BAD: Missing .catch()
109// fetchUser(1).then(user => console.log(user));
110// If fetchUser rejects → UnhandledPromiseRejection → process crash (Node 15+)
111
112// ✅ GOOD: Always handle errors
113fetchUser(1)
114 .then((user) => console.log(user))
115 .catch((err) => console.error(err));
116
117// ✅ BEST: Centralized error handling in chains
118function 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 chain
125 console.error("Order processing failed:", err.message);
126 throw err; // Re-throw if caller should handle it too
127 });
128}
129
130// 8. Creating pre-resolved/rejected Promises
131const resolved = Promise.resolve(42);
132const rejected = Promise.reject(new Error("boom"));
133rejected.catch(() => {}); // Handle to avoid warning
134
135// 9. Promisifying callback APIs
136const { promisify } = require("util");
137const execAsync = promisify(require("child_process").exec);
138
139// Helper mock functions
140function 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:

  1. Implement a retry(fn, maxAttempts) function using Promises that retries a failing async operation
  2. Use Promise.all() to fetch data from 5 different sources in parallel and merge the results
  3. Use Promise.allSettled() to build a health-check that tests 5 services and reports which are up/down
  4. Implement a timeout(promise, ms) wrapper using Promise.race()
  5. Build a basic Promise-based task queue that limits concurrency (max 3 simultaneous operations)
  6. 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() receives undefined instead of the intended value

  • Nesting .then() inside .then() — this recreates callback hell; always return Promises for flat chaining

  • Not 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 — use Promise.allSettled() instead to get all results

  • Creating 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