ADR-006: Seven canonical error categories, by convention not by type
Status: Accepted Date: 2026-04-21
Context
When a service logs an error, the Phase 5 alerting story needs to group errors into a small, named set of categories so that SLO burn-rate alerts can target "error rate of upstream_timeout over the last 15 minutes" without false-positives from unrelated failures (validation rejections, not-found lookups). Without a taxonomy, every service invents its own error labels and dashboards become per-service hand-tuning forever.
The categories must be:
- Small enough to reason about. A reviewer reading a dashboard needs to hold the set in their head.
- Separable by alert intent.
validationfailures andupstream_timeoutfailures warrant different alerts (one is "the client should retry", the other is "the dependency is sick"). - Stable across services. A cross-service dashboard comparing error mix only works if every service uses the same names.
The library's observ.Error(err).Category(...).Log(...) chain already exposes this as a first-class step; see gyrum-go pkg/observ/README.md.
Decision
Every ERROR-level log line carries an err_category field. The canonical set is seven values:
| Category | When |
|---|---|
validation |
Input failed validation before business logic ran. |
auth |
Caller is unauthenticated or unauthorised. |
not_found |
Target resource does not exist. |
upstream_timeout |
Outbound call exceeded its deadline. |
upstream_error |
Outbound call returned non-2xx or a library error. |
db_error |
DB call failed for any reason other than timeout. |
internal |
Unhandled, unexpected, or a violated invariant. Bug class. |
The category is free-form at the library boundary — Category("foo") compiles. Conformance is convention, not wall. Services agree to the seven-name set; the library does not enforce it.
Extension requires an issue against gyrum-labs/gyrum-go titled observ: propose new error category "<name>", rationale, and a use site. The bar is "the existing seven forced me into a lie", not "I wish this category existed". See observability-standards.md#error-categories.
Consequences
- SLO alerts target specific categories. Phase 5 alerts read
{service="distill", err_category="upstream_timeout"}and only fire when the targeted class of failure spikes. A burst of validation rejections from a buggy client does not page on-call for a dependency. - Cross-service dashboards work. A factory-wide "error mix by category" panel compares like with like across every service.
- Category conformance is a review question, not a compile error. Call sites with
Category("weird_one_off")passgo build. A reviewer catches them. The CI linter mentioned in ADR-008 will flag unknown categories once it ships; until then, the discipline is manual. - The set is intentionally short of specific failure modes. There is no
network_errorseparate fromupstream_error, norate_limitedseparate fromupstream_error. When specificity matters, add a field (upstream_status_code,retry_after_ms), don't add a category. Categories are the group-by axis for alerts; fields are the detail. - Adding the eighth category requires friction. An issue, a rationale, an ADR amendment. This is deliberate — category growth beyond ~10 values starts to dilute the alert-routing benefit the taxonomy exists for.
Alternatives considered
- Free-form error strings. Whatever each caller passes in. Rejected: dashboards become per-service bespoke, and
{err=~".*timeout.*"}is not a design, it's a regex wish. Alert queries lose precision as the string space diverges across services. - Strict Go enum type (
observ.CategoryValidation,observ.CategoryAuth, ...). Compile-time enforcement. Rejected: the ergonomics cost at every call site is significant, and library upgrades that add a category break every consumer's build. The library's stated stance — "discipline not wall" — accepts some non-conformance in exchange for ergonomics and upgrade-safety. A lint (future work, tied to ADR-008) is the pragmatic middle ground. - HTTP-status-like numeric codes. Dense, grouping-friendly. Rejected: a reader scanning a dashboard should not have to translate
4015to "validation failed on date range". Human-readable names win. - Per-service taxonomies. Each service defines its own. Rejected: kills the cross-service alerting and dashboard story that is the whole point.
Supersedes: none Superseded by: