# `xerrors` > Structured application errors with stable codes, cause chaining, and zero-dependency log enrichment. **Module:** `code.nochebuena.dev/go/xerrors` **Tier:** 0 — zero external dependencies, stdlib only **Go:** 1.25+ **Dependencies:** none --- ## Overview `xerrors` provides a single error type — [`Err`](#err) — that carries a machine-readable [`Code`](#codes), a human-readable message, an optional cause, and optional key-value context fields. The `Code` values are stable string constants aligned with gRPC status codes. They are safe to persist, transmit in API responses, and switch on programmatically. The `httputil` module uses them to map errors to HTTP status codes automatically. This package does **not** handle HTTP responses, logging, or i18n. Those concerns belong to [`httputil`](../httputil) and [`logz`](../logz) respectively. ## Installation ```sh go get code.nochebuena.dev/go/xerrors ``` ## Quick start ```go import "code.nochebuena.dev/go/xerrors" // Create a structured error err := xerrors.New(xerrors.ErrNotFound, "user not found") // With cause chain err := xerrors.Wrap(xerrors.ErrInternal, "failed to query database", dbErr) // Convenience constructors (fmt.Sprintf-style) err := xerrors.NotFound("user %s not found", userID) // Builder pattern — attach structured context for logging err := xerrors.New(xerrors.ErrInvalidInput, "validation failed"). WithContext("field", "email"). WithContext("rule", "required") // Walk the cause chain with stdlib var e *xerrors.Err if errors.As(err, &e) { fmt.Println(e.Code()) // ErrInvalidInput fmt.Println(e.Message()) // "validation failed" } ``` ## Usage ### Creating errors | Function | Code | Use when | |----------|------|----------| | `New(code, message)` | any | general purpose | | `Wrap(code, message, err)` | any | wrapping a lower-level error | | `InvalidInput(msg, args...)` | `ErrInvalidInput` | bad or missing request data | | `NotFound(msg, args...)` | `ErrNotFound` | resource does not exist | | `Internal(msg, args...)` | `ErrInternal` | unexpected server-side failure | ### Attaching context fields Context fields are key-value pairs that enrich log records and debug output. They never appear in API responses. ```go err := xerrors.New(xerrors.ErrInvalidInput, "validation failed"). WithContext("field", "email"). WithContext("rule", "required"). WithContext("value", input.Email) ``` `WithContext` can be chained and called multiple times. Repeating a key overwrites the previous value. ### Cause chaining `Wrap` and `WithError` both set the underlying cause. `Err.Unwrap` is implemented, so `errors.Is` and `errors.As` walk the full chain: ```go err := xerrors.Wrap(xerrors.ErrInternal, "save failed", io.ErrUnexpectedEOF) errors.Is(err, io.ErrUnexpectedEOF) // true var e *xerrors.Err errors.As(err, &e) // true — works through fmt.Errorf("%w", ...) wrapping too ``` ### Reading errors ```go var e *xerrors.Err if errors.As(err, &e) { e.Code() // xerrors.Code — the typed error category e.Message() // string — the human-readable message e.Fields() // map[string]any — shallow copy of context fields e.Unwrap() // error — the underlying cause e.Detailed() // string — verbose debug string: "code: X | message: Y | cause: Z | fields: {...}" } ``` `Fields()` always returns a non-nil map and is safe to mutate — it is a shallow copy of the internal state. ### JSON serialization `Err` implements `json.Marshaler`. This is what `httputil` uses to write error responses: ```json { "code": "NOT_FOUND", "message": "user abc123 not found", "fields": { "id": "abc123" } } ``` `fields` is omitted when empty. ### Structured log enrichment (duck-typing bridge) `logz` automatically enriches log records when it receives an `*Err` — no import of `xerrors` needed by `logz`, and no import of `logz` needed here. The bridge works through two methods that `Err` exposes: ```go // Called by logz internally via errors.As — never call these directly. func (e *Err) ErrorCode() string // → "NOT_FOUND" func (e *Err) ErrorContext() map[string]any // → the raw fields map ``` Passing an `*Err` to `logger.Error(msg, err)` automatically adds `error_code` and all context fields to the log record. ## Codes Wire values are gRPC status code names. HTTP mapping is the transport layer's responsibility. | Constant | Wire value | HTTP status | |----------|-----------|-------------| | `ErrInvalidInput` | `INVALID_ARGUMENT` | 400 | | `ErrUnauthorized` | `UNAUTHENTICATED` | 401 | | `ErrPermissionDenied` | `PERMISSION_DENIED` | 403 | | `ErrNotFound` | `NOT_FOUND` | 404 | | `ErrAlreadyExists` | `ALREADY_EXISTS` | 409 | | `ErrGone` | `GONE` | 410 | | `ErrPreconditionFailed` | `FAILED_PRECONDITION` | 412 | | `ErrRateLimited` | `RESOURCE_EXHAUSTED` | 429 | | `ErrCancelled` | `CANCELLED` | 499 | | `ErrInternal` | `INTERNAL` | 500 | | `ErrNotImplemented` | `UNIMPLEMENTED` | 501 | | `ErrUnavailable` | `UNAVAILABLE` | 503 | | `ErrDeadlineExceeded` | `DEADLINE_EXCEEDED` | 504 | Wire values are **stable across versions** — do not change them. Adding new constants is non-breaking. `Code.Description()` returns a short human-readable description of any code. ## Design decisions **`Err` instead of `AppErr`** — the "App" prefix is redundant inside a package already named `xerrors`. `xerrors.Err` reads cleanly at call sites. **`Code` instead of `ErrorCode`** — same reasoning. `xerrors.Code` is more concise. **`Fields()` returns a defensive copy** — the internal map is not exposed directly. Callers who want read-only access to the raw map (e.g. `logz`) use `ErrorContext()`. Callers who need to manipulate the result use `Fields()`. **`EnsureAppError` dropped** — auto-wrapping arbitrary errors into a structured error hides the real cause and discourages explicit error handling. Use `errors.As` to check for `*Err` and handle each case intentionally. **Wire values aligned with gRPC** — switching to gRPC (or adding gRPC alongside HTTP) requires no translation layer for most codes. ## Ecosystem ``` Tier 0: xerrors ← you are here ↑ Tier 1: logz (duck-types xerrors — no direct import) valid (depends on xerrors for error construction) ↑ Tier 2: httputil (maps xerrors.Code → HTTP status) ↑ Tier 4: httpmw, httpauth, httpserver ``` Modules that consume `xerrors` errors without importing this package: `logz` (via `ErrorCode()` / `ErrorContext()` duck-typing). ## License MIT