Express: Обработка ошибок

Express имеет специальный тип middleware для обработки ошибок — функцию с сигнатурой (err, req, res, next), которая перехватывает все ошибки, переданные через next(err) или брошенные в async-обработчиках.

Зачем нужно

Без централизованной обработки ошибок каждый маршрут содержит дублирующий код try/catch и формирования ответа. Error middleware позволяет вынести логику обработки в одно место: логировать ошибки, формировать единый формат ответа, скрывать внутренние детали от клиента в production.

Где используется

  • REST API — единый формат ошибок { error, message, statusCode }
  • Валидация входных данных (Joi, Zod) — перехват ValidationError
  • Работа с БД — перехват ошибок уникальности, соединения
  • Авторизация — UnauthorizedError, ForbiddenError
  • Глобальный fallback — 500 Internal Server Error для непредвиденных ошибок

Основной контент

Базовый error middleware

// Регистрируется ПОСЛЕДНИМ, после всех маршрутов
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: err.message || 'Internal Server Error'
  });
});

Передача ошибки через next

app.get('/users/:id', (req, res, next) => {
  UserService.getById(req.params.id)
    .then(user => {
      if (!user) {
        const err = new Error('User not found');
        err.status = 404;
        return next(err);
      }
      res.json(user);
    })
    .catch(next); // передаём ошибку БД в error middleware
});

Кастомный класс ошибки

// errors/AppError.js
class AppError extends Error {
  constructor(message, status = 500) {
    super(message);
    this.status = status;
    this.name = this.constructor.name;
  }
}

class NotFoundError extends AppError {
  constructor(resource = 'Resource') {
    super(`${resource} not found`, 404);
  }
}

class ValidationError extends AppError {
  constructor(message) {
    super(message, 400);
  }
}

module.exports = { AppError, NotFoundError, ValidationError };

Обёртка для async-маршрутов

// utils/asyncHandler.js
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

// Использование
app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await UserService.getById(req.params.id);
  if (!user) throw new NotFoundError('User');
  res.json(user);
}));

Полный error middleware

// middleware/errorHandler.js
const { AppError } = require('../errors/AppError');

module.exports = (err, req, res, next) => {
  // Логирование
  if (process.env.NODE_ENV !== 'test') {
    console.error(`[${new Date.toISOString()}] ${err.name}: ${err.message}`);
  }

  // Известные ошибки приложения
  if (err instanceof AppError) {
    return res.status(err.status).json({
      error: err.name,
      message: err.message
    });
  }

  // Ошибки Mongoose уникальности
  if (err.code === 11000) {
    return res.status(409).json({ error: 'Conflict', message: 'Duplicate entry' });
  }

  // Неизвестная ошибка — скрываем детали в production
  res.status(500).json({
    error: 'InternalServerError',
    message: process.env.NODE_ENV === 'production' ? 'Something went wrong' : err.message
  });
};

404 для несуществующих маршрутов

// Ставится перед error middleware, после всех маршрутов
app.use((req, res, next) => {
  next(new NotFoundError(`Route ${req.method} ${req.path}`));
});

app.use(errorHandler); // error middleware последним

Частые ошибки

  • 4 параметра обязательны — если написать (err, req, res) без next, Express не распознает как error middleware
  • Регистрировать error middleware до маршрутов — он должен быть последним app.use
  • Не вызывать next после res.json — приведёт к "Cannot set headers after they are sent"
  • Забыть catch(next) в Promise-цепочках — необработанные rejections не попадут в error middleware
  • Отдавать stack trace в production — утечка внутренних деталей, уязвимость безопасности

Связанные темы

Ресурсы