Chapter 11: Node.js Async/Await
1. The most important sentence first
async / await is syntactic sugar that makes working with Promises look almost like synchronous code — but it still runs asynchronously.
It doesn’t change how the event loop works. It only changes how you write and how you read asynchronous code.
|
0 1 2 3 4 5 6 |
await = "pause only this function — never the whole program" |
That single sentence is the key to understanding everything.
2. Basic syntax – the three rules you must remember
Rule 1: Only async functions can use await
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Correct async function fetchData() { const response = await fetch('https://api.example.com/data'); return response.json(); } // Wrong – SyntaxError function fetchData() { const response = await fetch('...'); // ← illegal } |
Rule 2: await can only be used inside an async function (or top-level module with top-level await)
Rule 3: await only works on things that return a Promise
|
0 1 2 3 4 5 6 7 8 9 10 |
await 123; // allowed – but pointless (immediately resolves) await "hello"; // same await Promise.resolve(42); // useful await fetch(...); // very useful await db.query(...); // very useful |
3. Side-by-side comparison – same task four different ways
Task: Read a file, parse JSON, log it
A. Callback style (old-school)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
const fs = require('node:fs'); fs.readFile('settings.json', 'utf-8', (err, data) => { if (err) return console.error(err); let config; try { config = JSON.parse(data); } catch (e) { return console.error("Invalid JSON", e); } console.log("Config loaded:", config); }); |
B. Promise style (.then / .catch)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
const fs = require('node:fs/promises'); fs.readFile('settings.json', 'utf-8') .then(data => JSON.parse(data)) .then(config => { console.log("Config:", config); }) .catch(err => { console.error("Failed:", err.message); }); |
C. async / await style (modern & recommended)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
const fs = require('node:fs/promises'); async function loadSettings() { try { const data = await fs.readFile('settings.json', 'utf-8'); const config = JSON.parse(data); console.log("Config loaded:", config); return config; } catch (err) { console.error("Failed to load settings:", err.message); throw err; // or handle differently } } loadSettings().catch(err => { console.error("Top-level error:", err); }); |
D. Top-level await (very clean – Node.js 14+)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
// index.js import { readFile } from 'node:fs/promises'; const data = await readFile('settings.json', 'utf-8'); const config = JSON.parse(data); console.log("App started with config:", config); |
→ No need to wrap everything in an async function anymore
4. Real-world example – Express route (very common pattern)
|
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
import express from 'express'; import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); const app = express(); app.use(express.json()); app.get('/users/:id', async (req, res) => { try { const id = Number(req.params.id); const user = await prisma.user.findUnique({ where: { id }, include: { posts: { take: 5, orderBy: { createdAt: 'desc' } } } }); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json(user); } catch (err) { console.error('Database error:', err); // You can send different status codes based on error type if (err.code === 'P2025') { // Prisma "not found" return res.status(404).json({ error: 'User not found' }); } res.status(500).json({ error: 'Internal server error' }); } finally { // Optional: cleanup if needed } }); app.listen(3000, () => { console.log('Server running → http://localhost:3000'); }); |
Why this pattern is so popular:
- Looks almost synchronous → easy to read top-to-bottom
- Proper error handling with try/catch
- Clean control flow (early returns)
- Works perfectly with TypeScript / Prisma / Mongoose / etc.
5. The most important mental model – “await pauses only the function”
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
async function example() { console.log("1. Start"); await new Promise(r => setTimeout(r, 1500)); // fake delay console.log("2. After 1.5 seconds"); } example(); console.log("3. This line runs IMMEDIATELY – not waiting"); |
Output:
|
0 1 2 3 4 5 6 7 8 |
1. Start 3. This line runs IMMEDIATELY – not waiting 2. After 1.5 seconds |
→ The whole application keeps running while we wait
→ Only the current async function is paused
6. Running multiple operations in parallel (very important)
Slow (sequential – total time = a + b + c)
|
0 1 2 3 4 5 6 7 8 |
const user = await getUser(123); const posts = await getPosts(123); const comments = await getComments(123); |
Fast & clean (parallel – total time ≈ max(a,b,c))
|
0 1 2 3 4 5 6 7 8 9 10 |
const [user, posts, comments] = await Promise.all([ getUser(123), getPosts(123), getComments(123) ]); |
Even better – with named results:
|
0 1 2 3 4 5 6 7 8 9 10 |
const [user, posts, comments] = await Promise.all([ getUser(123), getPosts(123), getComments(123) ].map(p => p.catch(err => ({ error: err })))); // safer |
7. Most common beginner & intermediate mistakes
| Mistake | What happens | Fix / Better pattern |
|---|---|---|
| Forget to await | Get Promise { <pending> } | Always await or .then() |
| No try/catch around await | Unhandled rejection → process crash | Wrap in try/catch |
| return await vs return | Unnecessary microtask tick (tiny performance hit) | Prefer return promise unless you need to catch |
| Sequential awaits when parallel possible | Slow response times | Use Promise.all |
| Putting heavy sync CPU work inside async fn | Still blocks event loop | Move to Worker Threads |
| Using await in loops without care | Very slow (sequential) | Use Promise.all + .map() |
Bad loop:
|
0 1 2 3 4 5 6 7 8 |
for (const id of ids) { await processItem(id); // sequential – slow } |
Good:
|
0 1 2 3 4 5 6 |
await Promise.all(ids.map(id => processItem(id))); |
8. Quick reference – When to use what
| Situation | Best pattern (2026) | Why? |
|---|---|---|
| Most API routes, services, scripts | async / await + try/catch | Cleanest & most maintainable |
| Startup code (connect DB, load config) | Top-level await | Very clean |
| Parallel independent operations | Promise.all / Promise.allSettled | Much faster |
| Want results even if some fail | Promise.allSettled | Safer & more debuggable |
| First success wins | Promise.any | Try multiple sources |
| First to finish (success or fail) | Promise.race | Timeouts, fastest server |
Summary – Sentences to remember
- async function = allows await inside
- await promise = pause only this function until the promise settles
- try/catch is mandatory around await (unless you want to crash)
- Use Promise.all when tasks are independent
- Top-level await makes startup code beautiful
- Never do long synchronous CPU work in the main thread — it blocks everything
Which direction would you like to go next?
- Real Express / Fastify API with proper error handling & async patterns
- Converting callback-based code to async/await
- Parallel + sequential patterns in real services
- Handling timeouts with Promise.race
- Debugging stuck / hanging async code
- async/await + streams or events
Just tell me what feels most useful right now — I’ll continue with realistic examples. 😄
