Structured application errors with typed codes, cause chaining, key-value context fields, and zero-import logz enrichment bridge. What's included: - `*Err` type implementing error, errors.Unwrap, json.Marshaler, ErrorCode(), and ErrorContext() - Twelve typed Code constants aligned with gRPC canonical status names - New / Wrap factory constructors plus InvalidInput / NotFound / Internal convenience constructors - Builder methods WithContext and WithError for attaching structured fields and causes - Duck-typed ErrorCode() / ErrorContext() bridge so logz auto-enriches log records without an import Tested-via: todo-api POC integration Reviewed-against: docs/adr/
3.6 KiB
xerrors
Structured application errors with stable typed codes, cause chaining, and key-value context fields.
Purpose
xerrors provides a single error type — *Err — that carries a machine-readable
Code, a human-readable message, an optional cause, and optional key-value fields.
It replaces ad-hoc string errors and sentinel variables with a consistent,
structured, JSON-serialisable error model that works across service boundaries,
log pipelines, and HTTP transports.
Tier & Dependencies
Tier: 0
Imports: encoding/json, fmt (stdlib only)
Must NOT import: logz, rbac, launcher, or any other micro-lib module.
The logz bridge is achieved via duck-typed private interfaces — no import required.
Key Design Decisions
- Typed error codes as a
stringtype alias — stable wire values aligned with gRPC status names. Seedocs/adr/ADR-001-typed-error-codes.md. *ErrimplementsUnwrap,ErrorCode,ErrorContext, andjson.Marshalerfor full stdlib compatibility and automatic log enrichment. Seedocs/adr/ADR-002-stdlib-errors-compatibility.md.- Twelve codes cover the gRPC canonical set (
INVALID_ARGUMENT,NOT_FOUND,INTERNAL,ALREADY_EXISTS,PERMISSION_DENIED,UNAUTHENTICATED,GONE,FAILED_PRECONDITION,RESOURCE_EXHAUSTED,CANCELLED,UNIMPLEMENTED,UNAVAILABLE,DEADLINE_EXCEEDED).
Patterns
Creating errors:
// Primary factory
err := xerrors.New(xerrors.ErrNotFound, "user not found")
// Convenience constructors (most common codes)
err := xerrors.NotFound("user %s not found", userID)
err := xerrors.InvalidInput("email is required")
err := xerrors.Internal("unexpected database state")
// Wrapping a cause
err := xerrors.Wrap(xerrors.ErrInternal, "failed to query database", dbErr)
// Builder pattern for structured context
err := xerrors.New(xerrors.ErrInvalidInput, "validation failed").
WithContext("field", "email").
WithContext("rule", "required").
WithError(cause)
Inspecting errors:
var e *xerrors.Err
if errors.As(err, &e) {
switch e.Code() {
case xerrors.ErrNotFound:
// handle 404
case xerrors.ErrInvalidInput:
// handle 400
}
}
Cause chain traversal:
// errors.Is and errors.As walk through *Err via Unwrap
if errors.Is(err, sql.ErrNoRows) { ... }
JSON serialisation (API responses):
// *Err marshals to: {"code":"NOT_FOUND","message":"user not found","fields":{...}}
json.NewEncoder(w).Encode(err)
Automatic log enrichment (no extra code needed):
// If err is *Err, logz appends error_code and context fields automatically
logger.Error("request failed", err)
What to Avoid
- Do not match on
err.Error()strings. Always useerrors.As+e.Code(). - Do not add HTTP status code logic to this package. HTTP mapping belongs in the transport layer.
- Do not add new code constants unless they map to a gRPC canonical status name.
- Do not import
logzfrom this package. The duck-type bridge (ErrorCode,ErrorContext) keeps the two packages decoupled. ErrorContext()returns the live internal map — do not mutate it. UseFields()if you need a safe copy.
Testing Notes
compliance_test.gouses compile-time nil-pointer assertions to enforce that*Errsatisfies theerror,Unwrap,ErrorCode,ErrorContext, andjson.Marshalercontracts. These assertions have zero runtime cost.xerrors_test.gocovers construction, chaining, builder methods, anderrors.Is/errors.Asbehaviour.- No test setup is needed — all tests use plain Go with no external dependencies.