53 lines
2.4 KiB
Markdown
53 lines
2.4 KiB
Markdown
|
|
# 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.
|