Go Best Practices

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 .