Express.js Error Handling

Implement robust error handling in Express.js with custom error classes, async error handling, and production-ready error responses.

Overview

Proper error handling is crucial for building reliable Express.js applications. Express provides mechanisms for catching and handling errors both synchronously and asynchronously.

Synchronous Errors

Express automatically catches synchronous errors thrown in route handlers and middleware. These errors are passed to the error-handling middleware.

Asynchronous Errors

For async operations, you need to explicitly pass errors to next() or use async/await with proper try-catch blocks.

Error-Handling Middleware

Error-handling middleware has four parameters: err, req, res, next. Express recognizes it as error-handling middleware by this signature.

Custom Error Classes

Creating custom error classes helps organize different types of errors and allows for consistent error responses.

Production vs Development

In development, you might want detailed error information. In production, hide implementation details and show user-friendly messages.

Code Examples

Custom Error Class

utils/AppError.js
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
    this.isOperational = true;

    Error.captureStackTrace(this, this.constructor);
  }
}

// Usage in routes
app.get('/user/:id', async (req, res, next) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    return next(new AppError('User not found', 404));
  }
  res.json(user);
});

Async Error Wrapper

utils/catchAsync.js
// Wrapper for async route handlers
const catchAsync = (fn) => {
  return (req, res, next) => {
    fn(req, res, next).catch(next);
  };
};

// Usage
app.get('/users', catchAsync(async (req, res) => {
  const users = await User.find();
  res.json(users);
}));

// Errors are automatically passed to error handler

Global Error Handler

middleware/globalErrorHandler.js
const globalErrorHandler = (err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'error';

  if (process.env.NODE_ENV === 'development') {
    res.status(err.statusCode).json({
      status: err.status,
      error: err,
      message: err.message,
      stack: err.stack
    });
  } else {
    // Production: don't leak error details
    if (err.isOperational) {
      res.status(err.statusCode).json({
        status: err.status,
        message: err.message
      });
    } else {
      console.error('ERROR:', err);
      res.status(500).json({
        status: 'error',
        message: 'Something went wrong'
      });
    }
  }
};

module.exports = globalErrorHandler;

Frequently Asked Questions

How do I handle errors in async middleware?
Wrap your async middleware in a try-catch block and call next(error) in the catch block, or use a utility function like catchAsync to automatically forward errors.
Should I handle all errors the same way?
No. Distinguish between operational errors (expected, like invalid input) and programming errors (bugs). Handle them differently - operational errors can be shown to users, programming errors should be logged and a generic message shown.
How do I handle uncaught exceptions and unhandled rejections?
Use process.on('uncaughtException') and process.on('unhandledRejection') to catch these globally. Log them and gracefully shut down your server.

Need expert help with Express.js?

Our team at Slashdev.io builds production-ready Express.js applications.