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:
- Understanding different patterns
- Proper error handling
- Efficient execution strategies
- Best practices for performance
Practice these patterns to write better Node.js code! 🎯