NODE.JS DEVELOPMENT:Day 7: Async Patterns — Callbacks, Promises, and Async/Await
Mastering day 7: async patterns — callbacks, promises, and async/await concepts and implementation.
Async Patterns in Node.js
Node.js is single-threaded but handles I/O concurrently through its event loop. Understanding async patterns is essential for writing performant Node.js applications.
The Event Loop
Node.js processes async operations in phases:
- timers: setTimeout and setInterval callbacks
- I/O callbacks: most async I/O callbacks
- idle, prepare: internal
- poll: new I/O events (blocking if queue empty and no timers)
- check: setImmediate callbacks
- close callbacks: socket.on('close', ...)
console.log('1: sync');
setTimeout(() => console.log('2: setTimeout'), 0);
setImmediate(() => console.log('3: setImmediate'));
Promise.resolve().then(() => console.log('4: microtask'));
console.log('5: sync');
// Output order: 1 → 5 → 4 → 2 → 3
// Microtasks (Promises) run before the next event loop phase
Callbacks — The Original Pattern
const fs = require('fs');
// Error-first callback convention
fs.readFile('data.json', 'utf8', (err, data) => {
if (err) {
console.error('Failed to read:', err.message);
return;
}
const json = JSON.parse(data);
// Callback hell — nested callbacks are hard to read
fs.writeFile('output.json', JSON.stringify(json, null, 2), (err) => {
if (err) {
console.error('Failed to write:', err.message);
return;
}
console.log('Done!');
});
});
Promises — Composable Async
const fs = require('fs').promises; // Node 10+ built-in promise API
function processFile(inputPath, outputPath) {
return fs.readFile(inputPath, 'utf8')
.then(data => JSON.parse(data))
.then(json => ({ ...json, processed: true }))
.then(result => JSON.stringify(result, null, 2))
.then(output => fs.writeFile(outputPath, output))
.then(() => console.log('Done!'));
}
// Promise.all — run in parallel, wait for all
Promise.all([
fetch('https://api.example.com/users'),
fetch('https://api.example.com/posts'),
]).then(([usersRes, postsRes]) => {
return Promise.all([usersRes.json(), postsRes.json()]);
}).then(([users, posts]) => {
console.log(users, posts);
});
Async/Await — The Modern Standard
const fs = require('fs').promises;
async function processFile(inputPath, outputPath) {
try {
const data = await fs.readFile(inputPath, 'utf8');
const json = JSON.parse(data);
const result = { ...json, processed: true };
await fs.writeFile(outputPath, JSON.stringify(result, null, 2));
return result;
} catch (err) {
if (err.code === 'ENOENT') throw new Error(`File not found: ${inputPath}`);
throw err; // Re-throw unexpected errors
}
}
// Parallel execution with async/await
async function fetchDashboardData(userId) {
const [user, posts, notifications] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchNotifications(userId),
]);
return { user, posts, notifications };
}
// Sequential when order matters
async function onboardUser(data) {
const user = await createUser(data); // Must happen first
const profile = await createProfile(user.id); // Needs user.id
await sendWelcomeEmail(user.email); // Can be parallel with profile creation
return { user, profile };
}
Common Async Patterns
Retry with exponential backoff
async function withRetry(fn, maxAttempts = 3, baseDelayMs = 300) {
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err;
if (attempt < maxAttempts) {
const delay = baseDelayMs * Math.pow(2, attempt - 1); // 300, 600, 1200ms
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
const data = await withRetry(() => fetch('https://api.example.com/data').then(r => r.json()));
Rate limiting with a queue
class RateLimitedQueue {
constructor(requestsPerSecond) {
this.interval = 1000 / requestsPerSecond;
this.queue = [];
this.running = false;
}
async add(fn) {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
if (!this.running) this.run();
});
}
async run() {
this.running = true;
while (this.queue.length > 0) {
const { fn, resolve, reject } = this.queue.shift();
try { resolve(await fn()); } catch (e) { reject(e); }
await new Promise(r => setTimeout(r, this.interval));
}
this.running = false;
}
}
const queue = new RateLimitedQueue(5); // Max 5 requests/second
await queue.add(() => fetch('https://api.example.com/item/1'));
Stream processing for large files
const { createReadStream, createWriteStream } = require('fs');
const { Transform } = require('stream');
const readline = require('readline');
async function processLargeCSV(inputPath, outputPath) {
const rl = readline.createInterface({ input: createReadStream(inputPath) });
const out = createWriteStream(outputPath);
out.write('id,name,processed\n'); // Header
for await (const line of rl) {
const [id, name] = line.split(',');
const processed = name?.trim().toUpperCase();
out.write(`${id},${processed},true\n`);
}
out.end();
console.log('Processing complete');
}
// Streams handle gigabyte files without loading into memory
await processLargeCSV('10gb-file.csv', 'output.csv');
Hands-on Examples
Promise Combinators — allSettled, race, any
// Promise.all — fail fast on any rejection
const results = await Promise.all([p1, p2, p3]);
// Promise.allSettled — wait for all, regardless of failures
const settled = await Promise.allSettled([p1, p2, p3]);
settled.forEach(result => {
if (result.status === 'fulfilled') console.log('OK:', result.value);
else console.log('ERR:', result.reason);
});
// Promise.race — resolve/reject with first settled
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 5000));
const result = await Promise.race([fetch(url), timeout]);
// Promise.any — resolve with first fulfilled (ignores rejections)
const fastestMirror = await Promise.any([
fetch('https://mirror1.example.com/file'),
fetch('https://mirror2.example.com/file'),
fetch('https://mirror3.example.com/file'),
]);allSettled is best for batch operations where partial success is OK. race is ideal for timeouts. any is perfect for trying multiple sources.
Progress