Files
jellybit/docs/conventions/errors.md
T

6.6 KiB
Raw Blame History

Ошибки

Конвенция: как устроены и передаются ошибки в jellybit. Правила оформления кода (How). Где и когда ошибку логировать — в 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.
  • Публичный канал — пользовательские поверхности (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.