Files
jellybit/docs/conventions/errors.md
T

93 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ошибки
Конвенция: *как* устроены и передаются ошибки в jellybit. Правила оформления
кода (How). Где и когда ошибку **логировать** — в [logging.md](logging.md),
раздел «Ошибки» (коротко: лог один раз на доменной границе). Здесь — как
ошибки строятся, оборачиваются и проверяются.
## Базовая идиома: stdlib
- Только стандартный `errors` + `fmt.Errorf`. Без `pkg/errors` (в режиме
поддержки) и `cockroachdb/errors` (стек-трейсы/Sentry — избыточно для
домашнего сервиса). Контекст ошибки несёт `slog`, а не стек.
- Если отладка начнёт упираться в «где именно родилась ошибка» — это сигнал
пересмотреть, а не дефолт.
## Обёртка и контекст
jellybit — **приложение, а не библиотека**: внешнего Go-API нет, весь код
наш. Поэтому внутри приложения обёртка `%w`**дефолт**, чтобы `errors.Is`/
`errors.As` работали сквозь слои.
- Добавляем контекст обёрткой: `fmt.Errorf("parse magnet: %w", err)`.
- `%w` — когда вызывающий может инспектировать причину (наш обычный случай).
`%v` — когда причину сознательно **не** раскрываем (не хотим завязывать
вызывающего на чужой тип ошибки).
- От утечки внутренних ошибок наружу защищаемся **не** через `%v` в цепочке,
а трансляцией на внешней границе (см. ниже).
Стиль сообщения:
- со строчной, без точки в конце, без «failed to»/«error» — обёртка и так
читается как «контекст: причина»;
- контекст — операция/субъект: `"link target: %w"`, не `"something failed"`;
- без заикания: каждый слой добавляет **свой** смысл, не повторяет нижний
(`"add to qbt: %w"`, а не `"add download failed: add to qbt failed: …"`).
## Проверка ошибок
- Сравнение — только `errors.Is(err, ErrX)` (не `err == ErrX`) и
`errors.As(err, &target)`. **Никогда** не матчим по тексту
(`strings.Contains(err.Error(), …)`).
- Граничные ошибки зависимостей **транслируем в доменные у источника**:
`sql.ErrNoRows` → доменный `store.ErrNotFound` в слое store, чтобы выше по
коду не торчал `database/sql`.
## Sentinel vs типизированные
- **Sentinel** (`var ErrNotFound = errors.New("not found")`) — для условий,
на которые ветвится код (нет записи, дубликат по infohash,
неподдерживаемый источник). Проверяем `errors.Is`.
- **Типизированная ошибка** (тип с полями + метод `Error()`) — когда
вызывающему нужны **данные** ошибки (поле валидации, код). Достаём
`errors.As`. Не плодим типы там, где хватает sentinel.
## Граница и трансляция: приватный vs публичный канал
Внутри — богатые обёрнутые ошибки. На внешней границе ошибку **транслируем**,
и форма зависит от канала, кто его видит:
- **Приватный канал — логи** (владелец сервиса). Полная ошибка со всей
цепочкой `%w` и контекстом. Пишется один раз на доменной границе — см.
[logging.md](logging.md).
- **Публичный канал — пользовательские поверхности** (Telegram, web-UI, HTTP
API; ими пользуется не только владелец). Сюда отдаём:
- **человекочитаемое сообщение** по доменной ошибке — не сырой
`err.Error()` и не детали реализации (`database/sql`, пути, стек);
- **+ корреляционный ключ** для владельца — `download_id` (если операция
к загрузке) либо `request_id`, чтобы по нему найти полную ошибку в логах.
Пример: «При обработке загрузки произошла ошибка, download_id=12345», а
не «произошла ошибка» и не сырой текст;
- **маппинг доменной ошибки → статус/сообщение**: `ErrNotFound` → 404
«не найдено», валидация/`ErrNotMagnet` → 400 «некорректный источник»,
конфликт состояния (`ErrConflict` — операция недопустима в текущем
состоянии) → 409 «действие недоступно в текущем состоянии», прочее →
500 «внутренняя ошибка».
Граница публичная по умолчанию. Истинно приватный для владельца канал —
логи; отдельной «операторской» поверхности с сырыми ошибками не заводим.
## panic
- `panic` — только для невосстановимого: баг программиста (нарушенный
инвариант), ошибка инициализации, из которой нельзя стартовать.
- Не для управления потоком и не для ожидаемых ошибок (нет сети, плохой
ввод) — это значения `error`.
- `recover` — на верхней границе обработчика (HTTP middleware), чтобы один
паникующий запрос не ронял процесс.
## Несколько ошибок
- Сбор независимых ошибок (напр. валидация конфига — все проблемы разом) —
`errors.Join`; проверка собранного по-прежнему через `errors.Is`.