error-handling
Грамотное покрытие кода обработкой ошибок (try/catch/finally) и структурным логированием без «тихих» падений. Куда ставить try/catch (границы I/O/внешних вызовов/парсинга/транзакций) и куда НЕ ставить
SKILL.mdskills/error-handling/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 (Nextredirect()/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вокруг кода, который не бросает; необработанные промис-реджекты; незакрытые ресурсы при ошибке.