Chapter 8: Asynchronous
Asynchronous programming in Node.js — written as if I’m sitting next to you, explaining it step by step with analogies, real examples, common mistakes, and clear progression from beginner to intermediate understanding.
Let’s start from the very beginning and build up slowly.
1. The most important thing to understand first
JavaScript (and therefore Node.js) is single-threaded → only one piece of JavaScript code can run at any moment
But Node.js applications can handle thousands of users at the same time.
How is that possible?
→ Because almost everything that takes time is done asynchronously
The key idea:
“Don’t wait for slow things — tell JavaScript what to do when the slow thing finishes, and keep doing other work meanwhile.”
This is what asynchronous programming means in practice.
2. Analogy everyone understands: The restaurant waiter
Imagine you are the only waiter in a restaurant with 100 tables.
Synchronous (blocking) way (bad)
- Table 1 orders food → you go to kitchen → stand there and wait 10 minutes until food is ready
- Only after 10 minutes → you go back to table 1 → meanwhile 99 other tables are angry
Asynchronous (non-blocking) way (Node.js style — good)
- Table 1 orders food → you write it down → give it to kitchen → immediately go to table 2
- Table 2 orders → give to kitchen → go to table 3
- When kitchen finishes table 1 food → they ring a bell → you immediately pick it up and serve table 1
- You can serve hundreds of tables because you never wait in the kitchen
In Node.js:
- You = JavaScript thread (single)
- Kitchen = operating system / thread pool / network / database
- Bell = callback, Promise, async/await
3. The three main ways to write asynchronous code in Node.js (2025–2026)
| Style | Introduced | Still used in 2026? | Modern recommendation | Readability |
|---|---|---|---|---|
| Callbacks | 2009–2015 | Yes (legacy code) | Avoid new code | ★☆☆☆☆ |
| Promises | ~2015 | Yes | Good | ★★★☆☆ |
| async / await | 2017 (Node 8+) | Yes – dominant | Best for most cases | ★★★★★ |
Let’s see all three with the same real example: reading a file.
Example – Read file three different ways
1. Callback style (old-school)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
const fs = require('node:fs'); fs.readFile('data.txt', 'utf-8', (err, data) => { if (err) { console.error("Error:", err.message); return; } console.log("File content:", data); console.log("This runs after file is read"); }); console.log("This runs IMMEDIATELY – before file is read"); |
2. Promise style (cleaner)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
const fs = require('node:fs/promises'); fs.readFile('data.txt', 'utf-8') .then(data => { console.log("File content:", data); console.log("This runs after file is read"); }) .catch(err => { console.error("Error:", err.message); }); console.log("This runs IMMEDIATELY"); |
3. async / await style (most readable – recommended)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const fs = require('node:fs/promises'); async function readMyFile() { try { const data = await fs.readFile('data.txt', 'utf-8'); console.log("File content:", data); console.log("This runs after file is read"); } catch (err) { console.error("Error:", err.message); } } readMyFile(); console.log("This runs IMMEDIATELY"); |
Output order (all three cases):
|
0 1 2 3 4 5 6 7 8 |
This runs IMMEDIATELY File content: (whatever is in data.txt) This runs after file is read |
4. Why does await feel like synchronous code?
Because await pauses only the current async function — not the whole program.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
async function example() { console.log("Start"); await new Promise(resolve => setTimeout(resolve, 2000)); // fake 2-second delay console.log("After 2 seconds"); } example(); console.log("I am NOT blocked – I run right away"); |
Output:
|
0 1 2 3 4 5 6 7 8 |
Start I am NOT blocked – I run right away After 2 seconds ← only this waited |
This is the magic: the event loop keeps running while await is waiting.
5. Most common real-world asynchronous operations in Node.js
| Operation | Typical async pattern (2026) | Blocking if done sync? |
|---|---|---|
| Reading / writing files | fs/promises + await | Yes |
| Making HTTP requests (fetch) | await fetch(…) | Yes if sync (rare) |
| Database queries (Prisma, pg) | await prisma.user.findMany() | Yes |
| Waiting for timers | await new Promise(r => setTimeout(r, ms)) | No (timers always async) |
| Reading from stdin | process.stdin.on(‘data’, …) or streams | Yes if sync |
| Calling external APIs | await axios.get(…) | Yes |
| Child processes | exec, spawn callbacks / promises | Yes if sync |
6. Classic beginner mistakes (and how to fix them)
Mistake 1 – Forgetting to await
|
0 1 2 3 4 5 6 7 8 9 |
async function getUser() { const user = db.findUserById(5); // ← missing await! console.log(user); // → Promise { <pending> } } |
Fix:
|
0 1 2 3 4 5 6 |
const user = await db.findUserById(5); |
Mistake 2 – Not handling errors in async code
|
0 1 2 3 4 5 6 7 8 |
async function bad() { const data = await fs.readFile('not-exist.txt'); // → crashes process } |
Fix:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
async function good() { try { const data = await fs.readFile('not-exist.txt'); } catch (err) { console.error("File missing:", err.message); } } |
Mistake 3 – Sequential awaits when parallel is better
Slow:
|
0 1 2 3 4 5 6 7 8 9 |
const a = await fetchUser(1); const b = await fetchUser(2); const c = await fetchUser(3); // total time = a + b + c |
Fast:
|
0 1 2 3 4 5 6 7 8 9 10 11 |
const [a, b, c] = await Promise.all([ fetchUser(1), fetchUser(2), fetchUser(3) ]); // total time ≈ longest of a,b,c |
7. Quick reference – When to use what in 2026
| Situation | Best pattern (2026) |
|---|---|
| Simple scripts, one-off tasks | async / await |
| Top-level code (index.js) | Top-level await (Node 14+) |
| Legacy callback-based libraries | promisify + async / await |
| Very performance-sensitive code | Sometimes raw callbacks (rare) |
| Parallel operations | Promise.all, Promise.allSettled |
| Race conditions / fastest wins | Promise.race |
| Cleanup on exit | process.on(‘exit’), process.on(‘SIGINT’) |
Summary – One-liners to remember
- Synchronous = “I wait here until it’s done” → blocks everything
- Asynchronous = “Please do this when you can, call me when finished” → keeps going
- Node.js shines when most work is I/O (files, network, database)
- async / await is the most readable & maintainable way in 2025–2026
- Never block the event loop with long CPU work (use Worker Threads instead)
Would you like to go deeper into any of these areas?
- How to convert callback code to async/await
- Real example with Promise.all + error handling
- Understanding event loop + async together
- Top-level await in real index.js
- Common async patterns in Express APIs
- Debugging hanging promises or unhandled rejections
Just tell me which direction feels most useful — I’ll continue with full examples. 😄
