Chapter 10: Node.js Promises
Promises in Node.js — written as if I’m sitting next to you, explaining everything step by step with real examples, analogies, common mistakes, and the mental models that actually help people understand and use Promises correctly.
Let’s go slowly and build it properly.
1. What is a Promise really? (The honest explanation)
A Promise is a placeholder object that represents a future value.
It’s JavaScript’s built-in way of saying:
“I’m going to do something that takes time. I don’t have the result right now, but when I do, I will either:
- give you the successful result, or
- tell you what went wrong.”
So instead of waiting (blocking), you get a Promise object immediately, and you attach instructions saying “when you finish, please do this”.
This is the foundation of modern asynchronous JavaScript in Node.js.
2. Three possible states of a Promise
| State | Meaning | Can change? | Final? |
|---|---|---|---|
| pending | The operation is still running | Yes | No |
| fulfilled | The operation succeeded → has a value | No | Yes |
| rejected | The operation failed → has a reason (error) | No | Yes |
Once a Promise is fulfilled or rejected, it is settled — it can never change state again.
3. Creating a Promise (the basic syntax)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const myPromise = new Promise((resolve, reject) => { // Do something asynchronous here setTimeout(() => { const success = true; // imagine real work here if (success) { resolve("Everything worked! 🎉"); // success → fulfilled } else { reject(new Error("Something broke 😭")); // failure → rejected } }, 1500); }); console.log(myPromise); // → Promise { <pending> } |
- resolve(value) → promise becomes fulfilled with that value
- reject(reason) → promise becomes rejected with that reason (usually an Error)
4. The most important way to use a Promise: .then() and .catch()
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
myPromise .then((result) => { console.log("Success:", result); // runs when fulfilled }) .catch((error) => { console.error("Failed:", error.message); // runs when rejected }) .finally(() => { console.log("This always runs – success or failure"); }); |
Realistic output after 1.5 seconds:
|
0 1 2 3 4 5 6 7 |
Success: Everything worked! 🎉 This always runs – success or failure |
or if rejected:
|
0 1 2 3 4 5 6 7 |
Failed: Something broke 😭 This always runs – success or failure |
5. Real Node.js example – using the built-in Promise-based fs
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { readFile } from 'node:fs/promises'; console.log("Before reading file"); readFile('config.json', 'utf-8') .then((data) => { console.log("File content:", data); const config = JSON.parse(data); console.log("Parsed config:", config); }) .catch((err) => { console.error("Cannot read config:", err.message); }); console.log("After calling readFile – this runs immediately"); |
Output order:
|
0 1 2 3 4 5 6 7 8 9 |
Before reading file After calling readFile – this runs immediately File content: {"port":3000,"debug":true} Parsed config: { port: 3000, debug: true } |
This is non-blocking — the program keeps running while the file is being read.
6. The modern & recommended way: async / await + Promises
async / await is syntactic sugar on top of Promises — it makes them look almost synchronous.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
async function loadConfig() { try { console.log("→ Starting to read"); const data = await readFile('config.json', 'utf-8'); const config = JSON.parse(data); console.log("→ Loaded:", config); return config; } catch (err) { console.error("→ Failed to load config:", err.message); throw err; // or handle differently } finally { console.log("→ Cleanup or logging always happens"); } } loadConfig().catch((err) => { console.error("Top-level error:", err); }); console.log("→ This runs BEFORE the file is read"); |
Very important mental model:
|
0 1 2 3 4 5 6 |
await = "pause only this async function — not the whole program" |
The rest of your application (event loop) keeps running while we wait.
7. Chaining multiple Promises (very common pattern)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
async function processUser(userId) { try { const userResponse = await fetch(`https://api.example.com/users/${userId}`); const user = await userResponse.json(); const postsResponse = await fetch(`https://api.example.com/users/${userId}/posts`); const posts = await postsResponse.json(); return { user, posts }; } catch (err) { console.error("Processing failed:", err); throw err; } } |
This looks clean, but it waits sequentially — one request finishes before the next starts.
8. Running multiple Promises in parallel (much faster)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
async function getUserData(userId) { try { const [user, posts, comments] = await Promise.all([ fetch(`https://api.example.com/users/${userId}`).then(r => r.json()), fetch(`https://api.example.com/users/${userId}/posts`).then(r => r.json()), fetch(`https://api.example.com/users/${userId}/comments`).then(r => r.json()) ]); return { user, posts, comments }; } catch (err) { console.error("One or more requests failed:", err); throw err; } } |
Key points:
- Promise.all waits for all promises to settle
- If any promise rejects → whole Promise.all rejects
- Much faster when operations are independent
9. Other very useful Promise static methods (2025–2026)
| Method | What it does | When to use it |
|---|---|---|
| Promise.all() | Wait for all to succeed (or first failure) | Parallel independent tasks |
| Promise.allSettled() | Wait for all to finish (success or failure) | You want results even if some fail |
| Promise.race() | Returns the first promise that settles | Timeouts, fastest server response |
| Promise.any() | Returns the first fulfilled promise (ignores rejections) | Try multiple sources, take first success |
Example – allSettled is very popular in production:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const results = await Promise.allSettled([ readFile('a.txt'), readFile('b.txt'), readFile('missing.txt') ]); for (const result of results) { if (result.status === 'fulfilled') { console.log("Success:", result.value); } else { console.log("Failed:", result.reason.message); } } |
10. Common mistakes people make with Promises
| Mistake | What happens | Fix |
|---|---|---|
| Forgetting to return await | Silent bugs, wrong order | return await something or just return something |
| Not handling rejection | Unhandled promise rejection → crash | Always .catch() or try/catch |
| Using .then inside async function | Harder to read | Prefer await |
| Chaining many .then without error catch | Errors disappear in chain | Add .catch at the end or use try/catch |
| Doing CPU heavy work inside .then | Still blocks event loop | Move to Worker Threads |
Summary – Quick mental checklist
- Promise = future value container (pending → fulfilled / rejected)
- Use async / await + try/catch for most new code
- Use Promise.all when tasks are independent and you want speed
- Use Promise.allSettled when you need to know what failed
- Always handle errors — unhandled rejections crash Node.js
- await only pauses the current function, not the whole app
Would you like to go deeper into any of these areas?
- Converting old callback code to Promises / async-await
- Real Express route with proper Promise handling & error middleware
- Timeout pattern using Promise.race
- How to debug hanging / unresolved Promises
- Combining Promises with streams or events
- Common Promise anti-patterns in production
Just tell me what you want to see next — I’ll continue with full, realistic examples. 😄
