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/
2.4 KiB
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 theCode. logz checks for this interface viaerrors.Asand, when found, appends anerror_codefield to the log record automatically.ErrorContext() map[string]any— returns the raw context fields map. logz checks for this interface viaerrors.Asand 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)anderrors.As(err, &target)work through*Errboundaries 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
MarshalJSONshape (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.