Конвенция для обработки ошибок + рефакторинг кода
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# Ошибки
|
||||
|
||||
Конвенция: *как* устроены и передаются ошибки в 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`.
|
||||
Reference in New Issue
Block a user