Lesson 17 of 20

Error Handling

Async Error Handling Patterns

In Node.js, most errors occur in asynchronous operations — database queries, file reads, API calls. Proper error handling ensures your application does not crash and returns meaningful error messages to clients.

With async/await, use try/catch blocks to handle errors. For Express, you can create a wrapper function that automatically catches errors and passes them to the error middleware.

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

// Wrapper function to catch async errors automatically
function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

// Without wrapper — manual try/catch (repetitive)
app.get('/api/users', async (req, res, next) => {
  try {
    const users = await User.find();
    res.json(users);
  } catch (err) {
    next(err);
  }
});

// With wrapper — cleaner code
app.get('/api/users', asyncHandler(async (req, res) => {
  const users = await User.find();
  res.json(users);
}));

app.get('/api/users/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    const error = new Error('User not found');
    error.statusCode = 404;
    throw error;
  }
  res.json(user);
}));
  • try/catch — Wrap async operations to catch errors
  • next(err) — Pass errors to Express error middleware
  • asyncHandler — Wrapper that auto-catches async errors
  • throw new Error() — Create and throw custom errors
  • Always handle errors — unhandled rejections crash the app
Notes
  • Starting with Express 5, async errors in route handlers are automatically caught. In Express 4, you need the asyncHandler wrapper or try/catch.

Custom Error Classes and Global Handler

Custom error classes let you create specific error types with status codes and additional context. A global error handler middleware then processes all errors consistently.

This pattern keeps your route handlers clean — they just throw errors, and the global handler formats and sends the response.

Example
// utils/AppError.js — Custom error class
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true; // Expected errors (not bugs)
  }
}

module.exports = AppError;

// Usage in routes
const AppError = require('./utils/AppError');

app.get('/api/users/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    throw new AppError('User not found', 404);
  }
  res.json(user);
}));

app.post('/api/users', asyncHandler(async (req, res) => {
  if (!req.body.email) {
    throw new AppError('Email is required', 400);
  }
  const user = await User.create(req.body);
  res.status(201).json(user);
}));

// Global error handler (in app.js, after all routes)
app.use((err, req, res, next) => {
  console.error(`[ERROR] ${err.message}`);

  // Mongoose validation error
  if (err.name === 'ValidationError') {
    return res.status(400).json({
      error: 'Validation Error',
      details: Object.values(err.errors).map(e => e.message)
    });
  }

  // Mongoose cast error (invalid ObjectId)
  if (err.name === 'CastError') {
    return res.status(400).json({ error: 'Invalid ID format' });
  }

  // Our custom AppError
  if (err.isOperational) {
    return res.status(err.statusCode).json({ error: err.message });
  }

  // Unexpected errors — don't leak details
  res.status(500).json({ error: 'Something went wrong' });
});
Notes
  • Distinguish between operational errors (expected, like 'user not found') and programming errors (bugs). Only show detailed messages for operational errors.