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/
109 lines
3.6 KiB
Markdown
109 lines
3.6 KiB
Markdown
# 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 `string` type alias — stable wire values aligned with gRPC
|
|
status names. See `docs/adr/ADR-001-typed-error-codes.md`.
|
|
- `*Err` implements `Unwrap`, `ErrorCode`, `ErrorContext`, and `json.Marshaler` for
|
|
full stdlib compatibility and automatic log enrichment. See
|
|
`docs/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:**
|
|
|
|
```go
|
|
// 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:**
|
|
|
|
```go
|
|
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:**
|
|
|
|
```go
|
|
// errors.Is and errors.As walk through *Err via Unwrap
|
|
if errors.Is(err, sql.ErrNoRows) { ... }
|
|
```
|
|
|
|
**JSON serialisation (API responses):**
|
|
|
|
```go
|
|
// *Err marshals to: {"code":"NOT_FOUND","message":"user not found","fields":{...}}
|
|
json.NewEncoder(w).Encode(err)
|
|
```
|
|
|
|
**Automatic log enrichment (no extra code needed):**
|
|
|
|
```go
|
|
// 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 use `errors.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 `logz` from this package. The duck-type bridge (`ErrorCode`,
|
|
`ErrorContext`) keeps the two packages decoupled.
|
|
- `ErrorContext()` returns the live internal map — do not mutate it. Use `Fields()`
|
|
if you need a safe copy.
|
|
|
|
## Testing Notes
|
|
|
|
- `compliance_test.go` uses compile-time nil-pointer assertions to enforce that
|
|
`*Err` satisfies the `error`, `Unwrap`, `ErrorCode`, `ErrorContext`, and
|
|
`json.Marshaler` contracts. These assertions have zero runtime cost.
|
|
- `xerrors_test.go` covers construction, chaining, builder methods, and
|
|
`errors.Is`/`errors.As` behaviour.
|
|
- No test setup is needed — all tests use plain Go with no external dependencies.
|