errx
One structured error type with stable identity, class, redactable fields, return traces and a wire codec.
go get github.com/ubgo/errx
- Language
- Go
- Go version
- 1.24
- Core deps
- 0 direct
- Latest tag
- none
- License
- Apache-2.0
- Go modules
- 9
How it fits together
Highlights from the README
- Stable identity —
domain+code, separate from the human message, so dashboards and clients branch on it even when wording changes. - Expected / Defect / Cancelled class — middleware returns 4xx + retry, alerts on bugs, stays quiet on client-cancelled.
- Operator vs end-user message — internal detail never leaks to clients.
- Typed structured fields with per-field redaction — safe-by-default at any trust boundary; no redactable-string tax.
- Retry & severity on the error itself — resilience and logging layers read it, not a type switch.
- Lazy single-alloc stack + return-trace — ~15 ns capture, symbolized only when printed; plus where the error traveled.
- Suppressed errors — a failing
defer Close()never masks the real failure (a classic Go bug Go has no stdlib fix for). - Two-mode accumulation — collect every validation failure with field paths, not just the first.
Code from the README
err := errx.New("db: deadlock on user_orders").
WithDomain("billing").WithCode("TX_RETRY").
WithPublic("Something went wrong, please retry").
WithRetryable(0).
WithSafe("shard", "eu-1") // safe field — survives to logs/wire
// .With("token", secret) // unsafe field — auto-redacted at any boundary
httpx.Write(w, r, err) // → 503 application/problem+json
slog.Error("request failed", "err", err) // structured, redacted, fingerprintedpackage main
import (
"fmt"
"github.com/ubgo/errx"
)
func loadOrder(id string) error {
// ... db call fails ...
return errx.New("db: no rows for order " + id).
WithDomain("orders").
WithCode("NOT_FOUND").
WithPublic("Order not found").
WithHint("verify the order id").
WithSafe("order_id", id)
}
func main() {
err := loadOrder("o-42")
fmt.Println(err.Error()) // operator message (logs)
fmt.Println(errx.Get(err).Public("error")) // end-user-safe message
fmt.Println(errx.Code(err)) // NOT_FOUND
fmt.Println(errx.Get(err).Fingerprint()) // stable grouping key
}errx.New("…").
WithDomain("billing"). // namespace
WithCode("CARD_DECLINED"). // stable machine code
WithClass(errx.ClassExpected). // ClassExpected | ClassDefect | ClassCancelled
WithSeverity(errx.SevWarn). // SevDebug | SevInfo | SevWarn | SevError | SevFatal
WithRetryable(3 * time.Second). // retryable + suggested delay
WithOwner("payments") // routing/alertingerrx.New("pq: deadlock detected on user_orders"). // operator (logs, .Error())
WithPublic("Something went wrong, please retry"). // end-user-safe
WithLocalized("fr-FR", "Une erreur est survenue"). // i18n (BCP-47)
WithLocalized("ja", "エラーが発生しました")
e := errx.Get(err)
e.Public("fallback") // end-user message, never the operator detail
e.Localized("fr-CA", "fb") // → French message via language fallback (fr-CA → fr-FR)From the errx README.
At a glance (from the README)
| Module | Use it for | README |
|---|---|---|
contrib/httpx | RFC 9457 application/problem+json HTTP responses (zero extra deps) | docs |
contrib/grpc | google.rpc.Status + ErrorInfo/RetryInfo/LocalizedMessage/Help | docs |
contrib/connect | ConnectRPC errors with typed details | docs |
contrib/graphql | gqlgen ErrorPresenter (extensions.code) | docs |
contrib/sentry | Sentry capture with the core fingerprint as the grouping key | docs |
contrib/otel | OpenTelemetry exception span events + trace/span correlation | docs |
contrib/prometheus | errx_errors_total metrics by stable identity | docs |
contrib/goerr | Incremental migration from github.com/ubgo/goerr | docs |