Mastering Asynchronous Operations in Node.js: Practical Strategies

Asynchronous programming is at the heart of Node.js. Here are practical strategies to master async operations. Understanding Async Patterns Callbacks // Traditional callback pattern function readFile(callback) { fs.readFile('file.txt', 'utf8', (err, data) => { if (err) return callback(err); callback(null, data); }); } Promises // Promise-based function readFile() { return new Promise((resolve, reject) => { fs.readFile('file.txt', 'utf8', (err, data) => { if (err) reject(err); else resolve(data); }); }); } Async/Await // Modern async/await async function readFile() { try { const data = await fs.promises.readFile('file.txt', 'utf8'); return data; } catch (err) { throw err; } } Common Patterns Sequential Execution // Execute operations one after another async function sequential() { const user = await getUser(userId); const profile = await getProfile(user.id); const posts = await getPosts(user.id); return { user, profile, posts }; } Parallel Execution // Execute operations in parallel async function parallel() { const [user, profile, posts] = await Promise.all([ getUser(userId), getProfile(userId), getPosts(userId) ]); return { user, profile, posts }; } Race Conditions // Get first resolved promise async function race() { const result = await Promise.race([ fetchFromAPI1(), fetchFromAPI2(), timeout(5000) // Fallback ]); return result; } Error Handling Try-Catch with Async/Await async function handleErrors() { try { const data = await fetchData(); return data; } catch (error) { if (error.code === 'ENOENT') { // Handle file not found } else if (error.code === 'ECONNREFUSED') { // Handle connection refused } else { // Handle other errors } throw error; } } Promise Error Handling // Chain error handling fetchData() .then(data => processData(data)) .catch(error => { console.error('Error:', error); return fallbackData(); }) .finally(() => { cleanup(); }); Advanced Patterns Retry Logic async function retry(fn, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (i === maxRetries - 1) throw error; await sleep(1000 * (i + 1)); // Exponential backoff } } } Timeout Pattern function withTimeout(promise, ms) { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms) ) ]); } // Usage const data = await withTimeout(fetchData(), 5000); Batch Processing async function processBatch(items, batchSize = 10) { for (let i = 0; i < items.length; i += batchSize) { const batch = items.slice(i, i + batchSize); await Promise.all(batch.map(item => processItem(item))); } } Best Practices 1. Always Await Promises // Bad: Fire and forget asyncFunction(); // Unhandled promise // Good: Always await await asyncFunction(); 2. Use Promise.all for Independent Operations // Good: Parallel execution const [users, posts, comments] = await Promise.all([ getUsers(), getPosts(), getComments() ]); 3. Handle Rejections // Always handle promise rejections process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection:', reason); }); Conclusion Mastering async operations requires: ...

December 10, 2025 · 3700 views

Node.js Architecture Explained: Event Loop, Async Code, and Scaling

Understanding Node.js architecture is crucial for building scalable applications. Let’s dive into the event loop, async operations, and how Node.js handles concurrency. The Event Loop Node.js uses a single-threaded event loop to handle asynchronous operations efficiently. Event Loop Phases // Simplified event loop phases ┌───────────────────────────┐ │ ┌─────────────────────┐ │ │ │ Timers │ │ // setTimeout, setInterval │ └─────────────────────┘ │ │ ┌─────────────────────┐ │ │ │ Pending Callbacks │ │ // I/O callbacks │ └─────────────────────┘ │ │ ┌─────────────────────┐ │ │ │ Idle, Prepare │ │ // Internal use │ └─────────────────────┘ │ │ ┌─────────────────────┐ │ │ │ Poll │ │ // Fetch new I/O events │ └─────────────────────┘ │ │ ┌─────────────────────┐ │ │ │ Check │ │ // setImmediate callbacks │ └─────────────────────┘ │ │ ┌─────────────────────┐ │ │ │ Close Callbacks │ │ // socket.on('close') │ └─────────────────────┘ │ └───────────────────────────┘ How It Works // Example: Understanding execution order console.log('1'); setTimeout(() => console.log('2'), 0); Promise.resolve().then(() => console.log('3')); console.log('4'); // Output: 1, 4, 3, 2 // Why: // - Synchronous code runs first (1, 4) // - Microtasks (Promises) run before macrotasks (setTimeout) // - Event loop processes callbacks Asynchronous Operations Callbacks // Traditional callback fs.readFile('file.txt', (err, data) => { if (err) { console.error(err); return; } console.log(data); }); Promises // Promise-based fs.promises.readFile('file.txt') .then(data => console.log(data)) .catch(err => console.error(err)); // Async/await async function readFile() { try { const data = await fs.promises.readFile('file.txt'); console.log(data); } catch (err) { console.error(err); } } Event Emitters const EventEmitter = require('events'); class MyEmitter extends EventEmitter {} const myEmitter = new MyEmitter(); myEmitter.on('event', (data) => { console.log('Event received:', data); }); myEmitter.emit('event', 'Hello World'); Concurrency Model Single-Threaded but Non-Blocking // Node.js handles I/O asynchronously const http = require('http'); // This doesn't block http.get('http://example.com', (res) => { res.on('data', (chunk) => { console.log(chunk); }); }); // This continues immediately console.log('Request sent, continuing...'); Worker Threads for CPU-Intensive Tasks // For CPU-intensive operations const { Worker, isMainThread, parentPort } = require('worker_threads'); if (isMainThread) { const worker = new Worker(__filename); worker.postMessage('Start calculation'); worker.on('message', (result) => { console.log('Result:', result); }); } else { parentPort.on('message', (msg) => { // Heavy computation const result = performHeavyCalculation(); parentPort.postMessage(result); }); } Scaling Strategies Cluster Module const cluster = require('cluster'); const os = require('os'); if (cluster.isMaster) { const numWorkers = os.cpus().length; for (let i = 0; i < numWorkers; i++) { cluster.fork(); } cluster.on('exit', (worker) => { console.log(`Worker ${worker.id} died`); cluster.fork(); // Restart worker }); } else { // Worker process require('./server.js'); } Load Balancing // PM2 for process management // pm2 start app.js -i max // Or use nginx/HAProxy for load balancing Best Practices 1. Avoid Blocking the Event Loop // Bad: Blocks event loop function heavyComputation() { let result = 0; for (let i = 0; i < 10000000000; i++) { result += i; } return result; } // Good: Use worker threads or break into chunks function asyncHeavyComputation() { return new Promise((resolve) => { setImmediate(() => { // Process in chunks resolve(computeChunk()); }); }); } 2. Use Streams for Large Data // Bad: Loads entire file into memory const data = fs.readFileSync('large-file.txt'); // Good: Stream processing const stream = fs.createReadStream('large-file.txt'); stream.on('data', (chunk) => { processChunk(chunk); }); 3. Handle Errors Properly // Always handle errors in async operations async function fetchData() { try { const data = await fetch('http://api.example.com'); return data; } catch (error) { // Log and handle error console.error('Fetch failed:', error); throw error; } } Performance Tips 1. Connection Pooling // Database connection pooling const pool = mysql.createPool({ connectionLimit: 10, host: 'localhost', user: 'user', password: 'password', database: 'mydb' }); 2. Caching // Use Redis for caching const redis = require('redis'); const client = redis.createClient(); async function getCachedData(key) { const cached = await client.get(key); if (cached) return JSON.parse(cached); const data = await fetchData(); await client.setex(key, 3600, JSON.stringify(data)); return data; } 3. Compression // Enable gzip compression const compression = require('compression'); app.use(compression()); Common Pitfalls 1. Callback Hell // Bad: Nested callbacks fs.readFile('file1.txt', (err, data1) => { fs.readFile('file2.txt', (err, data2) => { fs.readFile('file3.txt', (err, data3) => { // Deep nesting }); }); }); // Good: Use async/await async function readFiles() { const [data1, data2, data3] = await Promise.all([ fs.promises.readFile('file1.txt'), fs.promises.readFile('file2.txt'), fs.promises.readFile('file3.txt') ]); } 2. Unhandled Promise Rejections // Always handle promise rejections process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection:', reason); // Log and handle }); Conclusion Node.js architecture is powerful because: ...

December 10, 2025 · 4092 views