Сервер, который падает от одной непойманной ошибки, или логи, в которых ничего нельзя найти, видно сразу. На собесе спрашивают, как ты делишь ошибки, где их ловишь и что пишешь в лог.
Два вида ошибок
- Операционные: ожидаемые сбои внешнего мира. База недоступна, таймаут, файл не найден, пользователь прислал невалидные данные. Их обрабатывают: повтор, ответ 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 через всю цепочку вызовов.