error-handling

Грамотное покрытие кода обработкой ошибок (try/catch/finally) и структурным логированием без «тихих» падений. Куда ставить try/catch (границы I/O/внешних вызовов/парсинга/транзакций) и куда НЕ ставить

SKILL.md

GitHub

Навык: Error Handling & Logging

Цель — надёжный код без «тихих» падений и с грамотной расстановкой логов: понятно, что упало, почему и с каким контекстом — без шума и утечек секретов.

Куда ставить try/catch (и куда НЕ ставить)

  • Ставь на границах, где ошибка ожидаема и её есть чем обработать: I/O (сеть, файлы, БД), внешние API, парсинг/десериализация (JSON.parse, zod), транзакции, места, где нужен cleanup.
  • НЕ оборачивай всё подряд. Программные ошибки (баги, нарушенные инварианты) пусть всплывают до глобального обработчика — не глотай их локально. Один осмысленный try на операцию лучше, чем try вокруг каждой строки.
  • Не дублируй обработку: ловишь и пробрасываешь — не логируй на каждом слое (см. «Логи один раз»).

Никаких «тихих» падений

  • Пустой catch {} запрещён. В catch всегда одно из трёх: (1) обработать — осмысленный фолбэк + лог; (2) пробросить (throw), возможно обогатив: throw new AppError('...', { cause: error }); (3) преобразовать в доменную ошибку/Result. «Проглотил и пошёл дальше» — только с явным комментарием, почему это безопасно.
  • Не теряй причину: new Error(msg, { cause }) сохраняет ссылку на исходную ошибку (её message/stack/тип доступны через err.cause), но не сшивает стеки в один трейс — логгер должен явно сериализовать цепочку cause (pino err-serializer это делает; console.log без util.inspect — нет).
  • В прикладном коде бросай только Error (или наследников), иначе теряются stack и тип. Исключение: фреймворки бросают спец-объекты для control-flow (Next redirect()/notFound()) — их не трогай и не проглатывай в catch без re-throw.
  • В TypeScript catch (error) имеет тип unknown — сузь перед использованием (error instanceof Error ? … : …), не обращайся к error.message вслепую.

Нормализация ошибок

  • Доменные классы: class AppError extends Error { code; httpStatus; context }. Отличай operational (ожидаемые: 4xx, недоступность внешнего) от programmer (баги → 5xx, алерт).
  • На границе наружу (HTTP) — единый error-handler: маппинг ошибки в статус + безопасное тело (без стека/внутренностей клиенту). Fastify — setErrorHandler; Next App Router — error.tsx на уровне сегмента, для ошибок корневого layout нужен global-error.tsx, а в route handlers/Server Actions ошибки нормализуй явно (структурированный ответ/throw с маппингом), не полагаясь на error.tsx.
  • Валидация входа (zod) на границе → предсказуемые 400, а не 500 глубоко внутри.
  • i18n сообщений: наружу отдавай стабильный машинный code (+ параметры), а человекочитаемый текст локализуй на границе по локали запроса; логи не локализуй (один язык, обычно English). Подробнее — скилл i18n.

Async, finally, cleanup

  • await внутри try; не смешивай .then().catch() с try/await бессистемно.
  • Освобождение ресурсов (коннект, файл, лок, таймер) — в finally, чтобы не текло при ошибке.
  • Параллельно: Promise.allSettled — когда важны все результаты; Promise.all — когда любой сбой должен прервать. На уровне процесса лови unhandledRejection/uncaughtException, но не продолжай работу: состояние процесса после них неопределённо — залогируй и заверши процесс (graceful shutdown → exit 1), пусть оркестратор/PM2/Docker перезапустит. Это не «глобальный catch, чтобы работать дальше».

Транзиентные сбои: ретраи/таймауты

  • Внешние вызовы — с таймаутом (AbortSignal.timeout). Ретрай — только на транзиентных ошибках (сеть/таймаут/5xx/429), с экспоненциальным бэкоффом + jitter и лимитом попыток; на 4xx/бизнес-ошибках не ретрай. Мутации ретрай только при идемпотентности по эффекту (idempotency key/дедуп на сервере), а не потому что «это просто повтор».
  • Каскады — circuit breaker/деградация; не держи пользователя в бесконечном ожидании.

Грамотная расстановка логов

  • Структурные логи (JSON: pino/структурный logger), не голый console.log в проде. Поля: level, msg, err (message+stack+cause), context (ids, операция), requestId/correlation id.
  • Уровни: error — сбой, требующий внимания; warn — аномалия с фолбэком; info — значимые бизнес-события (лог на каждый запрос — это отдельный access-log на выделенном канале/с сэмплингом, а не info по умолчанию, иначе шум); debug — детали диагностики (в проде выкл./по флагу). Не логируй всё на error.
  • Логируй один раз — на слое, где есть контекст и где ошибка обрабатывается (обычно верхняя граница/handler). Пробрасываешь — не логируй здесь же (иначе дубли на каждом уровне).
  • Что логировать: какая операция, ключевые id, длительность важных операций, исход; ошибку — целиком (err со stack и цепочкой cause — убедись, что сериализатор логгера разворачивает cause, иначе первопричина потеряется).
  • Что НЕ логировать: пароли, токены, ключи, PII, чувствительные тела/полный payload без нужды — маскируй/редактируй. На горячих путях — сэмплируй.
  • Логи не для управления потоком: не парси текст лога как сигнал.

Фронтенд (React)

  • Error boundary на дерево UI (класс-компонент или react-error-boundary): ловит только ошибки рендера/жизненного цикла/конструкторов дочерних компонентов — не ловит ошибки в event handlers, async (setTimeout/промисы), при SSR и в самом boundary. Показывает фолбэк, репортит.
  • Ошибки хендлеров/async и данных (fetch/react-query) — вне boundary: try/catch в хендлере, состояние error у react-query (повтор, дружелюбный UI; не «пустой экран» и не стек пользователю), глобальный window.onerror/onunhandledrejection.

Бэкенд (Node/Fastify)

  • Единый setErrorHandler — маппинг доменных ошибок → статус + безопасное тело; логирование там же (один раз).
  • Процессные обработчики (unhandledRejection, uncaughtException, сигналы) → лог + graceful shutdown (дренаж коннектов).
  • Не «съедай» ошибки в middleware/репозиториях молча.

Анти-паттерны (ищи и убирай)

  • catch (e) {} / catch (e) { console.log(e) } без обработки/проброса; catch, возвращающий null/undefined и скрывающий сбой.
  • throw 'строка'; потеря cause; логирование одной ошибки на 3 уровнях; лог секретов/PII; try вокруг кода, который не бросает; необработанные промис-реджекты; незакрытые ресурсы при ошибке.