# Обработка ошибок (обязательные правила)

## Модель и границы
- `error` — сигнал неуспеха; это не HTTP-ответ, не gRPC-status, не лог-запись и не “универсальный объект”.
- `%w` = контекст причины; формат ответа/коды/логирование делаются **только на границе**.
- Граница: HTTP error handler/middleware; gRPC interceptors (unary/stream) + recovery; entry-point джобы/воркера; fan-in (агрегация) параллельных задач.
- Логируем ошибку **один раз на границе**; ниже границы не логируем. Ниже границы только добавляем **смысловой** контекст.

## Доменные ошибки и typed errors
Доменная ошибка: термины предметной области; ожидаема; влияет на ветвление; не содержит HTTP/gRPC кодов.
Typed error: тип `error`, различаемый через `errors.As`, может хранить поля (`field/entity/id/...`).

Правила:
- Typed errors объявлять в одном месте (например, `internal/domain/errs`); запрещено: в transport-слое / “под один endpoint”.
- Typed error не содержит транспортных кодов, не формирует ответ, не логируется в месте создания, не прячет бизнес-логику в `Error()`.
- Проверка: `errors.As`/`errors.Is`. Запрещено: сравнение строк, `reflect.TypeOf`, парсинг текста.
- Публичные поля/детали для клиента решаются на границе (сообщения ошибок не контракт).

Рекомендуемый минимум: `Validation{Field,Msg}`, `NotFound{Entity,ID}`, `Unauthorized{Msg}`, `Forbidden{Msg}`, `Conflict{Msg}`.

Typed error нужен, если на границе требуется ветвление и/или нужны структурированные детали; иначе достаточно обычного `error` + контекст.

## Правила по слоям (кто что делает)
Цепочка: `Client -> Repository -> Service -> Transport handler -> Error boundary`.

Общее для всех слоёв:
- Возвращаем ошибку наверх; не “чинить” молча.
- Оборачиваем через `fmt.Errorf("...: %w", err)` только с полезным контекстом; “пустые” обёртки запрещены.
- Ошибки production-кода не игнорировать через `_`; если вызов теоретически может вернуть ошибку, её нужно обработать, вернуть наверх или явно перевести в контролируемую деградацию.
- Никаких транспортных ответов/кодов/логирования ниже границы.

Client: контекст вызова; при необходимости — нормализация внешних ошибок -> домен; без логов/транспорта.
Repository: инфраструктура -> домен (напр. `sql.ErrNoRows` -> `NotFound`); `%w` (операция+параметры); без логов; не терять первопричину.
Service: бизнес-валидация -> `Validation`; нормализация источников -> домен; без транспортных кодов/типов и без логов.
Transport handler: вызвать сервис и `return err` (без логов/маппинга/форматирования).

## HTTP + OpenAPI
Правила:
- OpenAPI-валидация делается **только** в middleware; handler’ы не валидируют типы/обязательность и не “проверяют” raw body.
- Реализация OpenAPI request validation см. `docs/design-guidelines/go/rest.md`. Ошибки валидатора нормализуем в единый безопасный контракт.
- Ошибки OpenAPI: 400 (или 422, но единообразно); стабильный безопасный контракт (напр. `message/loc/field`); в prod без внутренних текстов валидатора.
- Central HTTP error handler = единственная граница: статус+тело+логирование; порядок: `context.Canceled` -> “готовая” HTTP-ошибка фреймворка -> OpenAPI -> доменные typed -> остальное 500.
- Маппинг домена -> HTTP: Validation 400; Unauthorized 401; Forbidden 403; NotFound 404; Conflict 409; остальное 500.
- Логи: 5xx (БД/внешние/паники/сериализация/инварианты) = `error`; OpenAPI/бизнес-валидация/not found/`context.Canceled` не логировать как `error`; `context.DeadlineExceeded` обычно `warn` (504 — только если принято).
- Локальная HTTP-ошибка допустима только в handler для простых локальных проверок **одного** endpoint (не домен; не переиспользуется).

## gRPC
Правила:
- Граница = unary/stream interceptors: `error -> status` (codes+safe message+optional details) + логирование; recovery interceptor обязателен (panic -> `codes.Internal` + stacktrace в логе).
- Клиентский контракт: ветвление по `codes.*`; `message` безопасный; `details` только если принято (напр. валидация).
- Валидация: proto-типы + бизнес-валидация в service (`Validation`); если есть protoc/buf-валидаторы — единообразно маппить в `Validation`/`codes.InvalidArgument` на границе.
- Маппинг домена -> gRPC: Validation `InvalidArgument`; Unauthorized `Unauthenticated`; Forbidden `PermissionDenied`; NotFound `NotFound`; Conflict обычно `FailedPrecondition` (иногда `AlreadyExists`/`Aborted` — выбрать и зафиксировать); остальное `Internal`.
- Логи: всё, что уходит как `Internal` (и близкое), паники, системные сбои = `error`; ожидаемые доменные ошибки и `context.Canceled` не логировать как `error` (дедлайны чаще `warn`).
- `status.Error(...)` из handler допустим редко (ошибка строго gRPC-специфична; не домен; не нужна в HTTP).

## Параллельность (goroutines)
Правило: в параллельном коде граница = **fan-in (агрегация)**, а не тело goroutine.
- Goroutines возвращают `error` в агрегатор; не логируют и не форматируют ответ.
- Рекомендуется `errgroup.WithContext` (cancel-on-first по умолчанию).
- Collect-all: собирать список ошибок и объединять (например, `errors.Join`), затем логировать один раз на границе.
- Важные goroutines обязаны быть защищены `recover()` (panic -> error), иначе panic может не попасть в middleware/interceptor.
- Fire-and-forget запрещён по умолчанию; допустим только если результат не важен, риск приемлем, panic обработан.

## Деградация и фоллбэки
Если принято решение вернуть **успешный, но деградированный** результат:
- ошибку **не** пробрасываем наверх;
- логируем деградацию **в месте принятия решения** (не на границе) и **не дублируем** логирование;
- лог должен явно содержать `fallback/degraded` + причину + компонент.

## Запрещено (анти-паттерны)
- Логировать ниже границы (кроме оговорённых исключений/флагов).
- “Универсальная AppError” (status/code/details/...) как основной механизм.
- Привязывать service/repo к транспорту (HTTP/gRPC коды/типы).
- Отдавать клиенту `err.Error()` или “сырые” ошибки OpenAPI/валидатора (утечки деталей).
- Сравнивать ошибки по строке/типу через reflect; терять первопричину (нет `%w`); “пусто” оборачивать.
- Игнорировать ошибки через `_`, если это не закрытие ресурса в `defer` с отдельно обоснованной безопасностью.
- Делать typed error “под каждую мелочь”; sentinel с данными в тексте вместо typed error с полями.
- Формировать ответы/ошибки в каждом handler вместо централизованной границы.
- Использовать `panic` для ожидаемых сценариев (panic только для нарушения инвариантов и ловится recovery на границе).
- Логировать `context.Canceled` как ошибку.
- gRPC: отправлять “любая ошибка = Unknown/Internal” без маппинга доменной семантики.

## Допустимые исключения (строго ограниченно)
Исключение допустимо, только если упрощает код без поломки границ/контрактов/безопасности и без дублирования логов.
- Локальная HTTP-ошибка в handler: простая проверка одного endpoint, не домен.
- Локальная обработка ради fallback-ответа (200/OK): поведение ожидаемо, согласовано, документировано.
- Временное логирование 4xx: только под флагом/конфигом, без sensitive, не `error`, убрать после расследования.
- Подробные детали валидации: только dev/stage; в prod запрещено.
- gRPC handler возвращает `status.Error` напрямую: редко, когда ошибка строго gRPC-специфична.
- health/readiness/metrics: допускается минимальный формат вне общего контракта, важен корректный статус доступности.
- Нормализация нестабильных внешних ошибок в service: если провайдер “плавает” и нужен стабильный доменный сигнал.
- Временное логирование ниже границы при инциденте: только под флагом и на время инцидента; обязательно убрать и зафиксировать в тикете.
- `panic` при нарушении инварианта (ошибка программиста): ловится recovery на границе и маппится в internal.

## Шаблон “как писать новый код”
1) Repo: нормализация инфраструктуры -> домен; обёртка `%w` с полезным контекстом.
2) Service: бизнес-валидация -> `Validation`; нормализация; осмысленный контекст.
3) Handler: `return err` без логов/форматирования.
4) Boundary (HTTP/gRPC): маппинг + логирование + стабильный безопасный контракт ответа.
