Error handling
Layer-scoped error semantics with explicit mapping from domain errors to RPC codes .
Errors belong to layers
A request flows through four layers . Each layer owns a specific set of errors :
- Interceptors (
pkg/) :UNAUTHENTICATED,INVALID_ARGUMENT. Validation and auth happen here , before the handler is called . - Handlers (
internal/api/) :NOT_FOUND,ALREADY_EXISTS,PERMISSION_DENIED,PRECONDITION_FAILED. These are mapped from domain sentinel errors . - Domain services (
internal/domain/) :INTERNAL,UNAVAILABLE,DEADLINE_EXCEEDED. The domain never returns user-facing error codes directly . - Outbox workers (
internal/outbox/) : failures are retried by the queue . Workers log errors and rely on at-least-once delivery .
A handler returning INTERNAL for a missing resource ? Bug . An interceptor returning NOT_FOUND ? Design smell . The layer tells you where the error belongs .
Sentinel errors in the domain
Domain services raise Go sentinel errors (ErrNotFound , ErrAlreadyExists) . Handlers map these to RPC codes through an explicit mapping . No implicit conversions , no catch-all INTERNAL swallowing real errors .
Explicit error mapping
The handler declares a mapping from domain errors to RPC codes . This mapping is reviewable , auditable , and consistent across domains . When a new domain error is added , the mapping forces you to decide what the caller should see .