Lesson 6 of 6
5 min read
Node.js

Async Programming Patterns

Master callbacks, promises, and async/await for handling asynchronous operations

Why Async Matters in Node.js

Node.js is single-threaded and uses an event-driven, non-blocking I/O model. This means operations like reading files, making network requests, or querying databases don't block the main thread. Understanding async patterns is crucial for writing efficient Node.js code.

🔄 The Evolution of Async

Callbacks

Original way

Promises

ES6 (2015)

Async/Await

ES2017

Callbacks

The original async pattern in Node.js. A function passed as an argument to be called later.

const fs = require('fs');

// Callback pattern: error-first callback
fs.readFile('file.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('Error:', err);
    return;
  }
  console.log('Data:', data);
});

// Callback Hell (avoid this!)
fs.readFile('file1.txt', 'utf8', (err, data1) => {
  if (err) return console.error(err);
  fs.readFile('file2.txt', 'utf8', (err, data2) => {
    if (err) return console.error(err);
    fs.readFile('file3.txt', 'utf8', (err, data3) => {
      if (err) return console.error(err);
      console.log(data1, data2, data3);
      // This nesting can go on forever...
    });
  });
});

Promises

Promises represent a value that may be available now, later, or never. They have three states: pending, fulfilled, or rejected.

// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;
  
  setTimeout(() => {
    if (success) {
      resolve('Operation succeeded!');
    } else {
      reject(new Error('Operation failed'));
    }
  }, 1000);
});

// Using Promises
myPromise
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log('Done'));

// Promise with fs
const fs = require('fs').promises;

fs.readFile('file.txt', 'utf8')
  .then(data => console.log(data))
  .catch(err => console.error(err));

// Chaining Promises (solves callback hell)
fs.readFile('file1.txt', 'utf8')
  .then(data1 => {
    console.log(data1);
    return fs.readFile('file2.txt', 'utf8');
  })
  .then(data2 => {
    console.log(data2);
    return fs.readFile('file3.txt', 'utf8');
  })
  .then(data3 => {
    console.log(data3);
  })
  .catch(err => console.error(err));

Promise Utilities

// Promise.all - Wait for all promises to resolve
const promise1 = fs.readFile('file1.txt', 'utf8');
const promise2 = fs.readFile('file2.txt', 'utf8');
const promise3 = fs.readFile('file3.txt', 'utf8');

Promise.all([promise1, promise2, promise3])
  .then(([data1, data2, data3]) => {
    console.log('All files loaded');
  })
  .catch(err => console.error('One failed:', err));

// Promise.allSettled - Wait for all, even if some fail
Promise.allSettled([promise1, promise2, promise3])
  .then(results => {
    results.forEach((result, i) => {
      if (result.status === 'fulfilled') {
        console.log(`File ${i + 1}: Success`);
      } else {
        console.log(`File ${i + 1}: Failed - ${result.reason}`);
      }
    });
  });

// Promise.race - First one to resolve/reject wins
Promise.race([promise1, promise2])
  .then(data => console.log('First completed:', data));

// Promise.any - First one to resolve wins (ignores rejections)
Promise.any([promise1, promise2])
  .then(data => console.log('First success:', data));

Async/Await

The modern way to write async code. Makes promises look like synchronous code.

const fs = require('fs').promises;

// Basic async/await
async function readFile() {
  try {
    const data = await fs.readFile('file.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error('Error:', err);
  }
}

readFile();

// Sequential operations
async function readFiles() {
  try {
    const data1 = await fs.readFile('file1.txt', 'utf8');
    const data2 = await fs.readFile('file2.txt', 'utf8');
    const data3 = await fs.readFile('file3.txt', 'utf8');
    
    console.log(data1, data2, data3);
  } catch (err) {
    console.error('Error:', err);
  }
}

// Parallel operations with async/await
async function readFilesParallel() {
  try {
    const [data1, data2, data3] = await Promise.all([
      fs.readFile('file1.txt', 'utf8'),
      fs.readFile('file2.txt', 'utf8'),
      fs.readFile('file3.txt', 'utf8')
    ]);
    
    console.log(data1, data2, data3);
  } catch (err) {
    console.error('Error:', err);
  }
}

Error Handling Patterns

// Try/catch with async/await
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Failed to fetch:', error);
    throw error; // Re-throw if needed
  }
}

// Helper function for cleaner error handling
async function tryCatch(promise) {
  try {
    const data = await promise;
    return [null, data];
  } catch (error) {
    return [error, null];
  }
}

// Usage
async function main() {
  const [error, data] = await tryCatch(fetchData());
  
  if (error) {
    console.error('Error:', error);
    return;
  }
  
  console.log('Data:', data);
}

Practical Example: API Handler

const express = require('express');
const app = express();

// Async route handler
app.get('/users/:id', async (req, res) => {
  try {
    const user = await getUserById(req.params.id);
    const posts = await getPostsByUserId(user.id);
    const comments = await Promise.all(
      posts.map(post => getCommentsByPostId(post.id))
    );
    
    res.json({ user, posts, comments });
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Wrapper for async error handling
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.get('/products', asyncHandler(async (req, res) => {
  const products = await getProducts();
  res.json(products);
}));

app.listen(3000);

💡 Best Practices

  • • Always use async/await for new code
  • • Always wrap await in try/catch for error handling
  • • Use Promise.all() for parallel operations
  • • Avoid mixing callbacks and promises
  • • Remember: async functions always return a Promise

Continue Learning