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:
43
docs/adr/ADR-001-typed-error-codes.md
Normal file
43
docs/adr/ADR-001-typed-error-codes.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# ADR-001: Typed Error Codes
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-03-18
|
||||
|
||||
## Context
|
||||
|
||||
Services must communicate failure categories to callers — across HTTP responses,
|
||||
log records, and internal service-to-service calls — in a way that is stable,
|
||||
machine-readable, and meaningful to both humans and programs. Stringly-typed errors
|
||||
(string matching on `err.Error()`) are fragile: message text can change, comparisons
|
||||
are case-sensitive, and there is no compiler enforcement. Pure numeric codes (like
|
||||
HTTP status codes) are opaque without a lookup table.
|
||||
|
||||
## Decision
|
||||
|
||||
`Code` is declared as `type Code string`. Twelve constants are defined that map
|
||||
directly to gRPC status code names (e.g. `ErrInvalidInput = "INVALID_ARGUMENT"`,
|
||||
`ErrNotFound = "NOT_FOUND"`, `ErrInternal = "INTERNAL"`). Wire values are stable
|
||||
across package versions and are safe to persist, transmit in API responses, or
|
||||
switch on programmatically.
|
||||
|
||||
A single `New(code Code, message string) *Err` factory is the primary constructor;
|
||||
per-code constructors (`NotFound`, `InvalidInput`, `Internal`) are provided as
|
||||
convenience wrappers for the most common cases only. No separate constructor type
|
||||
exists per error code — the `Code` field carries that distinction.
|
||||
|
||||
HTTP mapping is intentionally **not** performed in this package. The transport
|
||||
layer (e.g. an HTTP middleware) is responsible for translating `Code` values to
|
||||
HTTP status codes. This keeps `xerrors` free of HTTP knowledge.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Callers switch on `err.Code()` rather than parsing `err.Error()` strings.
|
||||
Message text can be changed freely without breaking any switch statement.
|
||||
- `Code` values are safe to log, serialise to JSON, and embed in API contracts.
|
||||
- The string representation (`"NOT_FOUND"`, etc.) is readable in logs and JSON
|
||||
payloads without a separate lookup.
|
||||
- Adding new codes is non-breaking. Removing or renaming an existing code is a
|
||||
breaking change — because callers may switch on it.
|
||||
- The twelve initial codes cover the gRPC canonical set; codes outside that set
|
||||
are not defined here, keeping the surface small and the mapping to gRPC/HTTP
|
||||
unambiguous.
|
||||
52
docs/adr/ADR-002-stdlib-errors-compatibility.md
Normal file
52
docs/adr/ADR-002-stdlib-errors-compatibility.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# ADR-002: stdlib errors Compatibility
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-03-18
|
||||
|
||||
## Context
|
||||
|
||||
Go's `errors` package defines two key behaviours that all well-behaved error types
|
||||
must support: `errors.Is` for sentinel comparison and `errors.As` for type
|
||||
assertion down a cause chain. Code that wraps errors (e.g. a repository that wraps
|
||||
a database error) must not break these traversal mechanisms when it introduces its
|
||||
own error type.
|
||||
|
||||
Additionally, consumers — particularly HTTP handlers and log enrichers — need to
|
||||
extract typed information (`Code`, key-value fields) without performing unsafe type
|
||||
assertions directly in business logic.
|
||||
|
||||
## Decision
|
||||
|
||||
`*Err` implements `Unwrap() error`, which exposes the optional cause set via
|
||||
`Wrap(code, msg, cause)` or `.WithError(cause)`. This makes the full cause chain
|
||||
visible to `errors.Is` and `errors.As`.
|
||||
|
||||
Two private duck-typing interfaces are satisfied without any import dependency on
|
||||
the consumer packages:
|
||||
|
||||
- `ErrorCode() string` — returns the string value of the `Code`. logz checks for
|
||||
this interface via `errors.As` and, when found, appends an `error_code` field to
|
||||
the log record automatically.
|
||||
- `ErrorContext() map[string]any` — returns the raw context fields map. logz checks
|
||||
for this interface via `errors.As` and appends all key-value pairs to the log record.
|
||||
|
||||
`*Err` also implements `json.Marshaler`, producing
|
||||
`{"code":"...","message":"...","fields":{...}}` suitable for direct use in API
|
||||
error responses.
|
||||
|
||||
The compliance test (`compliance_test.go`) uses compile-time nil-pointer assertions
|
||||
to enforce these contracts. If any method is removed or its signature changes, the
|
||||
build fails immediately rather than at a distant call site in another module.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `errors.Is(err, io.ErrUnexpectedEOF)` and `errors.As(err, &target)` work through
|
||||
`*Err` boundaries without any special casing.
|
||||
- logz and xerrors are fully decoupled at the import level: neither imports the
|
||||
other. The duck-type bridge is maintained by the private interfaces.
|
||||
- `Fields()` returns a shallow copy for safe external use; `ErrorContext()` returns
|
||||
the raw map for logz's internal read-only use — a deliberate split to avoid
|
||||
allocating a copy on every log call.
|
||||
- The `MarshalJSON` shape (`code`, `message`, `fields`) is part of the public API
|
||||
contract. Changing field names is a breaking change for any caller that depends on
|
||||
the JSON representation.
|
||||
Reference in New Issue
Block a user