Files
xerrors/docs/adr/ADR-002-stdlib-errors-compatibility.md
Rene Nochebuena 3cc36801a1 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/
2026-03-18 13:09:31 -06:00

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 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.