TechLead
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