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! 🎯