- Docs
- Reference
- Errors
Errors
The repo error conventions for messages, sentinels, wrapping, and branching.
Canonical rules for error messages, sentinels, custom types, wrapping, and branching. Enforced forward-looking by make err-lint (tools/errscan); the guard is additive, not a cleanup of existing code.
Message format
Section titled “Message format”Every error message starts with <package>: <message>, a colon-space prefix naming the package or adapter that emits it. No brackets, no trailing period, no capitalization.
// goodvar ErrNotFound = errors.New("store: record not found")return fmt.Errorf("store: get %q: %w", key, err)
// badvar ErrNotFound = errors.New("record not found") // unprefixedvar ErrNotFound = errors.New("[E404] record not found") // bracket prefixvar ErrNotFound = errors.New("Store: record not found.") // capitalized, trailing periodSentinel errors
Section titled “Sentinel errors”- Named
ErrXxx, declared package-level, inerrors.go. - Doc comment required (exported identifier convention).
- Public API: renames and removals are breaking changes.
// ErrNotFound is returned when a lookup matches no record.var ErrNotFound = errors.New("store: record not found")Custom error types
Section titled “Custom error types”- Named
FooError; value receivers by default (error values stay comparable and safe to copy). - Implement
Unwrap() errorwhen the type wraps another error. - Carry structured data as fields; callers extract it with
errors.As/errors.AsType.
// ValidationError describes a single failed field check.type ValidationError struct { Field string Reason string}
func (e ValidationError) Error() string { return fmt.Sprintf("validate: field %q: %s", e.Field, e.Reason)}
func (e ValidationError) Unwrap() error { return ErrValidation }Wrapping
Section titled “Wrapping”- Wrap with
%wsoerrors.Is/errors.Askeep working through the chain. - Never put two
%wverbs in onefmt.Errorf; combine a sentinel with a cause usingerrors.Join. - Use
%vonly to deliberately break the chain at an abstraction boundary (the wrapped error is context, not something callers should match on).
// goodreturn fmt.Errorf("store: get %q: %w", key, err)return errors.Join(ErrNotFound, fmt.Errorf("store: get %q: %w", key, err))
// badreturn fmt.Errorf("store: %w: %w", ErrNotFound, err) // two %w verbsMulti-error
Section titled “Multi-error”errors.Joininside a singleUnwrap() erroris the default shape (for examplecontainer.Closejoining per-service close errors).Unwrap() []erroronly when callers genuinely need element-wise access.
Branching
Section titled “Branching”- Branch with
errors.Isanderrors.AsTypeonly. Never match on error strings, prefixes, or message content.
if errors.Is(err, ErrNotFound) { ... }
var target *ValidationErrorif errors.AsType[*ValidationError](err, &target) { ... }Go 1.26+ errors.AsType[E] is preferred for new code; errors.As is not deprecated and stays valid.
Panics
Section titled “Panics”- Panics are reserved for resolver-invariant guards in
dsl/backend/gogen/render_validate.go(states the resolver makes unreachable). The parser itself never panics. - Everything else returns an error.
Boundaries and security
Section titled “Boundaries and security”- Translate errors at package/service boundaries: map internal errors to safe, stable messages before they leave the process (HTTP responses, CLI output, RPC status).
- Never leak internal error text (DSNs, SQL, stack details, internal addresses) to external callers.
- Log once, structured, via
log/slogor the repo logger; redact secrets (config.Redact,redis.RedactAddr), and never log raw option maps.
func (s *Service) Handle(w http.ResponseWriter, r *http.Request) { if err := s.do(r.Context()); err != nil { s.log.ErrorContext(r.Context(), "service: handle", "err", err) http.Error(w, "internal error", http.StatusInternalServerError) return }}make err-lint (tools/errscan) scans the repo and fails on:
unprefixed:errors.New/fmt.Errorfliteral with no:in its first token (skipped inshared/apperror,_test.gofiles).bracket-prefix: literal starting with[...](outsideshared/apperror, which emits[CODE]messages).double-%w: two or more%win onefmt.Errorfformat string.panic:panic(...)outsidedsl/backend/gogen/render_validate.goand_test.gofiles.
Escape hatch: add an // errscan:allow comment on the offending line (or anywhere in the file for a file-level allow).