engineering-handbook

Error Handling in Express.js

Robust error handling is essential for building reliable, secure Node.js applications. This guide outlines our basic approach to centralizing error handling in our Node.js applications.

The error handler must have the following features:

The following example demonstrates the implementation of a basic error handler that logs all errors and prevents sending details about unexpected errors to the users. Expected errors (also called Blessed Errors) are forwarded to the end users, while unexpected errors are masked and presented as a 500 Internal Server Error, to avoid leaking sensitive information about the app architecture and potential vulnerabilities.

// errorHandler.js
import httpErrors from "http-errors";

function isBlessedError(err) {
  return err instanceof httpErrors.HttpError;
}

export function errorHandler() {
  return (err, req, res, next) => {
    if (res.headersSent) {
      return next(err);
    }

    var responseBody = { error: err.name };  

    if (err.message) {  
      responseBody.message = err.message  
    }  

    console.error("Error while handling request-id", req.headers.get('X-Request-ID'), err.code, err.name, err.message, err.stack, err.cause, err.details);

    res.status(err.code || 500).json(responseBody);
  };
}

With this implementation, the end user can only receive the error name and message, but developers can leverage err.cause and err.details to facilitate investigations and debugging. Developers must be aware that the information appended in the error message will be disclosed.

Registering the Error Handler

Add the error handler as the last middleware in your Express app:

// app.js
import express from 'express';
import { errorHandler } from './errorHandler.js';
import { router } from './router.js';

const app = express();

// Regular middleware
app.use(express.json());

// Routes
app.use('/api', router);

// Error handler (must be last)
app.use(errorHandler);

export default app;

Delegating Errors to the Handler

In route handlers, pass errors to Express’s next() function instead of handling them directly.

In an async handler async function (req, res) {} this is actually implied from express version 5 onwards. It helps simplify route handlers by removing the need for try/catch or next(err)

app.get('/users/:id', async (req, res) => {
  const user = await findUserById(req.params.id); // If this throws, Express 5 handles it
  res.json(user);
});

With the classic callback based interface, and in any express versions prior to version 4 (which does not support async handlers natively) you must pass it to next:

// controllers/site.js
export async function getSite(req, res, next) {
  try {
    const siteId = req.params.id;
    
    // Validate ID
    if (!siteId || !/^\d+$/.test(siteId)) {
      const error = new Error('Invalid site ID');
      error.statusCode = 400;
      return next(error);
    }
    
    const site = await siteService.getById(siteId);
    
    if (!site) {
      const error = new Error('Site not found');
      error.statusCode = 404;
      return next(error);
    }
    
    res.json(site);
  } catch (error) {
    // Pass unexpected errors to the error handler
    next(error);
  }
}