Обработка ошибок в маршрутах

Основные концепции

В современных веб-приложениях на JavaScript маршруты управляют обработкой входящих HTTP-запросов. Ошибки на этих уровнях могут возникать по разным причинам: некорректные данные, сбои при работе с базой данных, ошибки в логике приложения или внешние факторы. Эффективная обработка ошибок критична для обеспечения стабильности приложения и корректной работы клиентской стороны.

Ключевой момент: маршруты должны быть защищены от любых исключений и обеспечивать понятный ответ клиенту.

Обработка ошибок с использованием try...catch

Наиболее простой способ обработки ошибок — обернуть код маршрута в конструкцию try...catch:

app.get('/users/:id', async (req, res) => {
  try {
    const user = await getUserById(req.params.id);
    if (!user) {
      return res.status(404).json({ error: 'Пользователь не найден' });
    }
    res.json(user);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Внутренняя ошибка сервера' });
  }
});

Особенности:

  • try блок позволяет перехватывать синхронные и асинхронные ошибки (при использовании await).
  • catch блок обеспечивает централизованное логирование и формирование ответа клиенту.
  • Можно кастомизировать код ошибки и сообщение в зависимости от типа исключения.

Middleware для обработки ошибок

В Express и других фреймворках распространён подход с использованием middleware для централизованной обработки ошибок. Middleware для ошибок имеет четыре параметра: (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 || 'Неизвестная ошибка' });
});

Преимущества:

  • Снижается дублирование кода в маршрутах.
  • Можно легко добавлять логирование и мониторинг.
  • Обеспечивается единый формат ответа для всех ошибок.

Создание кастомных ошибок

Часто возникает необходимость передавать более детализированные ошибки. Для этого создаются собственные классы ошибок:

class NotFoundError extends Error {
  constructor(message) {
    super(message);
    this.name = 'NotFoundError';
    this.status = 404;
  }
}

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = 'ValidationError';
    this.status = 400;
  }
}

Использование в маршруте:

app.get('/products/:id', async (req, res, next) => {
  try {
    const product = await getProductById(req.params.id);
    if (!product) throw new NotFoundError('Продукт не найден');
    res.json(product);
  } catch (err) {
    next(err);
  }
});

Ключевой момент: передача ошибки через next(err) позволяет middleware централизованно обрабатывать все виды исключений.

Асинхронные маршруты и промисы

При работе с асинхронными функциями важно корректно обрабатывать промисы. Ошибка в промисе без await может остаться необработанной. Решением является использование обёрток:

const asyncHandler = fn => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.get('/orders/:id', asyncHandler(async (req, res) => {
  const order = await getOrderById(req.params.id);
  if (!order) throw new NotFoundError('Заказ не найден');
  res.json(order);
}));

Преимущество: весь асинхронный код автоматически передаёт ошибки в middleware, без необходимости дублировать try...catch в каждом маршруте.

Логирование и мониторинг ошибок

Для крупных приложений важно не только возвращать клиенту корректный ответ, но и фиксировать ошибки для анализа. Варианты:

  • console.error — базовое логирование.
  • Внешние сервисы (Sentry, LogRocket, Datadog) — централизованный сбор ошибок и алерты.
  • Файловое логирование через winston или pino — для аудита и анализа.

Совет: всегда включать уникальные идентификаторы ошибок и трассировки стека для упрощения отладки.

Ответы клиенту

Важно, чтобы клиент получал информативный, но безопасный ответ:

  • Для ошибок 4xx — сообщать о причине, например ValidationError: Неверные данные.
  • Для ошибок 5xx — давать общую информацию без раскрытия внутренних деталей, например Внутренняя ошибка сервера.
res.status(err.status || 500).json({
  error: err.publicMessage || 'Произошла ошибка на сервере'
});

Ключевой момент: разграничение внутреннего сообщения (err.message) и публичного (err.publicMessage) предотвращает утечку чувствительной информации.

Обработка ошибок на уровне роутеров

Для крупных проектов маршруты объединяются в роутеры. Каждый роутер может иметь собственное middleware для ошибок:

const userRouter = express.Router();

userRouter.get('/:id', asyncHandler(async (req, res) => {
  const user = await getUserById(req.params.id);
  if (!user) throw new NotFoundError('Пользователь не найден');
  res.json(user);
}));

userRouter.use((err, req, res, next) => {
  console.error('[User Router Error]', err);
  res.status(err.status || 500).json({ error: err.message });
});

app.use('/users', userRouter);

Преимущество: локальная обработка ошибок позволяет добавлять специфические правила для отдельных модулей без влияния на глобальное поведение приложения.