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

Содержание

О проверке кода. Примеры не запускались в этой сессии (по договорённости — фокус на контенте, а не прогонах). Говорим прямо, а не ставим бейдж «проверено».

Ошибки в API неизбежны: не тот ввод, недоступная БД, баг. Вопрос — как Express на них реагирует. Хаотичный try/catch в каждом маршруте — плохо; централизованный обработчик — правильно. Разберём механизм.

Особое middleware с четырьмя аргументами

Express распознаёт обработчик ошибок по числу параметров — их четыре, а не три:

// обычное middleware — 3 аргумента
app.use((req, res, next) => { /* ... */ });

// обработчик ОШИБОК — 4 аргумента, первый err
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: 'Что-то пошло не так' });
});

Именно по четырём аргументам (err, req, res, next) Express понимает, что это обработчик ошибок, и вызывает его только при ошибке. Ставят его в самом конце, после всех маршрутов и middleware — тогда он ловит ошибки со всех них в одном месте. Это заменяет разбросанный по маршрутам try/catch единой точкой.

Путь ошибки к обработчику
  1. 1 В маршруте ошибкаБД недоступна, throw, next(err)
  2. 2 Express пропускает обычные middlewareИщет обработчик с 4 аргументами
  3. 3 Обработчик ошибок (err, req, res, next)Логирует, формирует ответ
  4. 4 Единый ответ клиентуСтатус + понятное сообщение

Передача ошибки: next(err)

app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await db.users.findById(req.params.id);
    if (!user) {
      return next(new NotFoundError('Пользователь не найден'));  // передаём в обработчик
    }
    res.json(user);
  } catch (err) {
    next(err);   // ошибку БД — тоже в обработчик
  }
});

next(err) с аргументом-ошибкой перескакивает сразу к обработчику ошибок, минуя остальные обычные middleware. Это способ сказать «здесь проблема, разберись централизованно». В синхронном коде можно и просто throw — Express поймает сам. Но в асинхронном есть нюанс.

Ловушка async-ошибок

// ❌ Express 4: ошибка в async НЕ попадёт в обработчик автоматически
app.get('/users', async (req, res) => {
  const users = await db.query();   // если бросит — повиснет, обработчик не вызовется
  res.json(users);
});

// ✅ Express 4: ловим и передаём вручную
app.get('/users', async (req, res, next) => {
  try {
    const users = await db.query();
    res.json(users);
  } catch (err) {
    next(err);   // обязательно, иначе ошибка потеряется
  }
});

В Express 4 ошибка внутри async-функции не подхватывается автоматическиотклонённый промис не доходит до обработчика, запрос зависает. Её ловят try/catch и передают через next(err). Чтобы не оборачивать каждый маршрут, используют обёртку:

// обёртка: ловит async-ошибки и сама зовёт next
const asyncH = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

app.get('/users', asyncH(async (req, res) => {
  const users = await db.query();   // ошибка сама уйдёт в обработчик
  res.json(users);
}));

В Express 5 это исправлено — отклонённый промис уходит в обработчик ошибок автоматически, обёртка не нужна. Но проектов на Express 4 ещё много, поэтому знать про ловушку важно.

Свои классы ошибок

Чтобы обработчик знал, какой код вернуть, ошибкам дают тип и статус:

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
  }
}
class NotFoundError extends AppError {
  constructor(msg = 'Не найдено') { super(msg, 404); }
}
class ValidationError extends AppError {
  constructor(msg) { super(msg, 400); }
}

Свои классы ошибок несут статус-код, и обработчик использует его вместо жёсткого 500:

app.use((err, req, res, next) => {
  const status = err.statusCode || 500;
  logger.error(err);                          // полный стек — в лог
  res.status(status).json({
    error: err.message || 'Внутренняя ошибка',
  });
});

Теперь next(new NotFoundError()) даёт клиенту 404, а new ValidationError() — 400, автоматически и единообразно.

Не показывайте детали клиенту

// ❌ утечка внутренностей
res.status(500).json({ error: err.stack });   // стек виден клиенту!

// ✅ клиенту — общее, стек — в лог
app.use((err, req, res, next) => {
  logger.error(err);   // полный стек и контекст — на сервере
  const isProd = process.env.NODE_ENV === 'production';
  res.status(err.statusCode || 500).json({
    error: isProd ? 'Внутренняя ошибка' : err.message,
  });
});

В продакшене клиенту отдают общее сообщение, а полный стек логируют. Показывать стек и внутренние детали наружу — утечка информации о структуре приложения, помогающая злоумышленнику. В разработке детали полезны, поэтому режим переключают по NODE_ENV. Ошибки же логируют всегда — иначе о сбоях никто не узнает.

Практический чек-лист

  • один обработчик ошибок с 4 аргументами — в самом конце;
  • async-ошибки: try/catch + next(err) или обёртка (Express 4);
  • свои классы ошибок со статус-кодами — единообразные ответы;
  • клиенту — общее сообщение, в лог — полный стек;
  • различайте операционные ошибки и баги — как и в чистом Node;
  • всегда возвращайте корректный HTTP-статус.

Смежные темы: middleware — обработчик ошибок это особое middleware; обработка ошибок в Node; классы ошибок; REST API и статусы. Полный список — в уроках Node.js.

Частые вопросы

Как работает обработка ошибок в Express?
Через специальное middleware с четырьмя аргументами (err, req, res, next). Express распознаёт его по числу аргументов и вызывает, когда где-то передан next с ошибкой. Оно ловит ошибки со всех маршрутов в одном месте.
Чем error middleware отличается от обычного?
У него четыре параметра вместо трёх — первым идёт err. Именно по количеству аргументов Express понимает, что это обработчик ошибок, и вызывает его только при ошибке. Ставят его в самом конце, после всех маршрутов.
Как передать ошибку в обработчик?
Вызвать next с ошибкой: next(err). Express пропустит остальные обычные middleware и передаст управление сразу обработчику ошибок. Просто бросить исключение в синхронном коде тоже работает, Express его поймает.
Почему async-ошибки не попадают в обработчик?
В старых версиях Express ошибка внутри async-функции не подхватывалась автоматически — её нужно ловить try/catch и передавать через next. В Express 5 это исправлено, отклонённый промис уходит в обработчик ошибок сам.
Как не показывать детали ошибки клиенту?
В продакшене клиенту отдают общее сообщение и статус, а полный стек логируют на сервере. Показывать стек и внутренние детали опасно — это утечка информации о структуре приложения, полезной злоумышленнику.