feat(xerrors): initial stable release v0.9.0
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/
This commit is contained in:
108
CLAUDE.md
Normal file
108
CLAUDE.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user