Урок по треку Node.js и бэкенд 7 мин

Ошибки и логирование в Node.js

Операционные и программные ошибки, unhandledRejection, graceful shutdown, структурные логи.

Сервер, который падает от одной непойманной ошибки, или логи, в которых ничего нельзя найти, видно сразу. На собесе спрашивают, как ты делишь ошибки, где их ловишь и что пишешь в лог.

Два вида ошибок

  • Операционные: ожидаемые сбои внешнего мира. База недоступна, таймаут, файл не найден, пользователь прислал невалидные данные. Их обрабатывают: повтор, ответ 4xx/5xx, запасной путь.
  • Программные: баги. undefined is not a function, неверный аргумент. Их не «обрабатывают», а исправляют. Если такая ошибка вылетела непойманной, состояние процесса не гарантировано.

Где ловить

try/catch ловит только синхронные ошибки и ошибки из await в том же блоке:

try {
  setTimeout(() => { throw new Error('x'); }, 0); // НЕ поймается
  await fetchUser(id);                            // поймается
} catch (e) { ... }

Ошибку в колбэке таймера или EventEmitter try снаружи не видит: к моменту броска стек уже другой.

В Express 4 ошибка в async-обработчике не доходила до error-middleware, её нужно было передавать в next(err) вручную. Express 5 сам ловит отклонённые промисы из обработчиков. Обработчик ошибок в Express узнаётся по четырём аргументам: (err, req, res, next). В NestJS ту же роль играют exception filters.

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

class NotFoundError extends Error {
  constructor(entity, id, options) {
    super(`${entity} ${id} not found`, options);
    this.name = 'NotFoundError';
    this.status = 404;
  }
}

try { await repo.get(id); }
catch (e) { throw new NotFoundError('User', id, { cause: e }); }

Опция cause (ES2022) сохраняет исходную ошибку: в логах видна вся цепочка. Бросай только экземпляры Error: у строки нет стека.

Глобальные обработчики

  • process.on('unhandledRejection'): промис отклонён, а обработчика нет. С Node 15 по умолчанию это падение процесса с кодом 1.
  • process.on('uncaughtException'): синхронное исключение никто не поймал.

Правильная реакция на оба: залогировать, отправить в систему ошибок (Sentry, GlitchTip) и завершить процесс, а перезапустит его оркестратор. Продолжать работу после uncaughtException опасно: незакрытые транзакции, захваченные ресурсы, полуобновлённое состояние.

Логирование

  • Структурные логи: JSON-строка на событие, а не свободный текст. Тогда Loki, ELK или любое хранилище логов умеет фильтровать по полям.
  • Уровни: error, warn, info, debug. В проде обычно info и выше.
  • Пиши в stdout, а сбор и ротацию оставь инфраструктуре (Docker, Kubernetes).
  • Request id: каждому запросу присваивают идентификатор и добавляют в каждую строку лога. Тогда по одному id видно всю историю запроса, включая вызовы других сервисов. Протаскивать его через все функции помогает AsyncLocalStorage из node:async_hooks.
  • Без секретов и персональных данных: пароли, токены, номера карт, полные тела запросов. Логгеры умеют маскировать поля (redact).
  • Быстрые логгеры вроде pino пишут JSON с минимальными накладными расходами. console.log в горячем пути с большими объектами заметно тормозит.
logger.info({ reqId, userId, durationMs: 42 }, 'order created');

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

  • Пустой catch {}, который глотает ошибку.
  • Логировать одну и ту же ошибку на каждом уровне стека: в логе пять копий.
  • Отдавать клиенту стек и текст внутренней ошибки.
  • process.on('uncaughtException', log) без выхода: процесс живёт в сломанном состоянии.
  • throw 'строка' вместо Error.

Что спросят на собесе

  • Чем операционные ошибки отличаются от программных.
  • Что делать в uncaughtException и unhandledRejection.
  • Почему try/catch не ловит ошибку из setTimeout.
  • Как устроить логирование, чтобы по нему можно было расследовать инцидент.
  • Как передать request id через всю цепочку вызовов.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.