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

Обработчик ошибок — middleware с четырьмя аргументами (err, req, res, next), единое место для всех сбоев.
«Хорошее приложение не падает от ошибки — оно ловит её в одном месте и вежливо отвечает клиенту.»

Ошибки неизбежны: база недоступна, данные кривые, файл не найден. Без централизованной обработки придётся в каждом маршруте писать try/catch и руками формировать ответ. Express предлагает изящное решение — специальный error-handling middleware, через который проходят все ошибки приложения.

Особый middleware

Обработчик ошибок отличается от обычного middleware числом аргументов: их четыре, и первый — err:

// обычный middleware:  (req, res, next)
// обработчик ошибок:   (err, req, res, next)

app.use((err, req, res, next) => {
  console.error(err.stack);
  const status = err.status || 500;
  res.status(status).json({ error: err.message || 'Внутренняя ошибка' });
});

Express узнаёт обработчик ошибок именно по четырём аргументам и вызывает его, когда где-то происходит ошибка. Он должен стоять последним, после всех маршрутов.

Как ошибка попадает в обработчик

Есть два пути. Первый — явный вызов next(err). Второй (в Express 5) — выброс ошибки из async-функции:

// Express 4: нужен try/catch и next(err)
app.get('/old', async (req, res, next) => {
  try {
    const data = await loadData();
    res.json(data);
  } catch (err) {
    next(err);  // вручную в обработчик ошибок
  }
});

// Express 5: отклонённый промис уходит в обработчик САМ
app.get('/new', async (req, res) => {
  const data = await loadData();  // если упадёт -- поймает Express 5
  res.json(data);
});

Это одно из главных улучшений Express 5: для async-обработчиков больше не нужен try/catch в каждом маршруте.

Поток обработки ошибки

запрос
  |
  v
[ обработчик ] -- throw / next(err) -->
                                       |
   обычные middleware ПРОПУСКАЮТСЯ     |
                                       v
                         [ (err, req, res, next) ]
                                       |
                                       v
                              ответ с кодом ошибки

Собственный класс ошибки в браузере

Стандартный Error не несёт HTTP-статуса. Создадим свой класс с кодом — обработчик ошибок потом его прочитает:

class HttpError extends Error {
  constructor(status, message) {
    super(message);
    this.status = status;
  }
}

function findUser(id) {
  if (id !== 1) throw new HttpError(404, 'Пользователь не найден');
  return { id: 1, name: 'Аня' };
}

try {
  findUser(99);
} catch (e) {
  console.log('Статус:', e.status);   // 404
  console.log('Текст: ', e.message);  // Пользователь не найден
}

Такой класс — основа аккуратной обработки ошибок: бросаешь new HttpError(404, ...), а один обработчик превращает это в правильный ответ.

Как работает под капотом

Внутри Express отделяет обычные слои от error-слоёв по длине списка аргументов функции. Когда возникает ошибка, Express перестаёт вызывать обычные middleware и ищет следующий слой с четырьмя аргументами, передавая ему err. Если такого нет, срабатывает встроенный обработчик по умолчанию, который в проде скрывает детали ошибки.

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

  • Поставить обработчик не последним. Если он выше маршрутов, ошибки до него не дойдут.
  • Забыть про async в Express 4. Без try/catch отклонённый промис не попадёт в обработчик и станет unhandled rejection.
  • Показывать стектрейс клиенту. В проде отдавай общее сообщение, а детали — только в лог.

Best practices

  • Заведи один централизованный обработчик ошибок в конце приложения.
  • Используй классы ошибок со статусом для осмысленных ответов.
  • Логируй полную ошибку на сервере, клиенту отдавай безопасное сообщение.

Итоги

Error-handling middleware с четырьмя аргументами — единая точка обработки сбоев. В Express 5 async-ошибки попадают в него автоматически. Это завершает раздел про middleware; дальше соберём из всего изученного полноценный REST API.

Асинхронные ловушки и 404

Даже в Express 5 автоматический проброс работает только для ошибок внутри самого обработчика. Если ты запускаешь асинхронную операцию "в сторону" (например, в колбэке setTimeout без await), её отказ Express не поймает — это будет unhandled rejection уровня процесса. Поэтому всё, что относится к ответу, держи в основной async-цепочке с await. Отдельно стоит ситуация "маршрут не найден": это не ошибка, а нормальный исход, поэтому 404 обычно отдают не через обработчик ошибок, а отдельным завершающим middleware. Разделяй в голове две вещи: ожидаемые ответы вроде 404 и непредвиденные сбои, которые ловит error-handler.

Проверьте себя
1. Чем обработчик ошибок отличается от обычного middleware?
AОн быстрее
BУ него четыре аргумента, первый — err
CОн не вызывает next
DОн работает только с GET
2. Что нового даёт Express 5 для async-обработчиков?
AОни стали быстрее
BОтклонённый промис автоматически уходит в обработчик ошибок
CTry/catch теперь запрещён
DAsync больше не поддерживается