Treść z repozytorium z zachowaniem nagłówków, przykładów, kodu, tabel, linków i obrazów.
Persona: You are a Go engineer who treats errors as structured data. Every error carries enough context — domain, attributes, a trace — for an on-call engineer to diagnose it without hunting the developer.
Modes:
- Build — adding structured context to errors in a codebase.
- Review — checking that variable data lives in attributes (not messages), that layers wrap at package boundaries, and that goroutines recover.
When to use: any task centered on samber/oops. General error-wrapping philosophy lives in golang-error-handling; embedding slog values and APM conventions in golang-observability.
The core idea
samber/oops upgrades standard errors with machine-readable structure. The headline rule: variable data goes in attributes, never in the message. A message like "failed to process user u-123 in tenant acme" shatters APM grouping in Datadog/Loki/Sentry; the static message plus With("user_id", u) attributes keeps every instance of the same failure aggregated.
Attributes also travel with the error through the call stack — unlike adding slog attributes only at the log site, they survive wrapping and arrive fully-stocked at the error boundary.
The fluent builder
err := oops.
In("user-service").
Tags("database", "postgres").
Code("network_failure").
User("user-123", "email", "foo@bar.com").
With("query", query).
Errorf("failed to fetch user")Terminal methods: Errorf (new error), Wrap/Wrapf (wrap an existing error), Join (combine several), Recover/Recoverf (panic → error).
Builder slots worth knowing:
| Method | Purpose |
|---|---|
.With(k, v) | custom attribute; lazy func() any values supported |
.WithContext(ctx, ks...) | pull values out of a Go context into attributes |
.In(domain) | feature/service tag |
.Tags(...) | categorization — query later with err.HasTag |
.Code(slug) | machine-readable ID |
.Public(msg) | user-safe message, decoupled from technical detail |
.Hint(...) | runbook/pointer for the developer |
.Owner(team) | accountability marker |
.User(id, ...) / .Tenant(id, ...) | subject context |
.Trace(id) / .Span(id) | correlation ids (ULID when omitted) |
.Request(r, bool) / .Response(r, bool) | attach HTTP traffic |
.Since(t) / .Duration(d) | timing for the error |
oops.FromContext(ctx) | resume from a builder stashed in the context |
Layering: wrap once per boundary
Wrap* returns nil when given nil, so the nil-check wrapper is pure noise:
// ✓
return oops.Wrapf(err, "product lookup failed")
// ✗
if err != nil {
return oops.Wrapf(err, "product lookup failed")
}
return nilAdd context once per architectural layer, not on every function call:
func Handler() error { return oops.In("http").Trace(traceID).Wrapf(Service(), "create user failed") }
func Service() error { return oops.In("service").With("op", "create_user").Wrapf(Repo(), "db failed") }
func Repo() error { return oops.In("repo").Tags("postgres").Errorf("connection timeout") }Low-cardinality everywhere:
// ✗ high-cardinality; every value re-fragments the group
oops.Errorf("failed on user %s in tenant %s", uid, tid)
// ✓ static message + structured attributes
oops.With("user_id", uid).With("tenant_id", tid).Errorf("failed to process user")Panics at goroutine boundaries
Recover converts a panic into an error, and it is non-negotiable anywhere a goroutine starts:
func Process(data []byte) (err error) {
return oops.In("data-processor").Code("panic_recovered").
With("input_bytes", len(data)).
Recover(func() { risky(data) })
}Reading the result
oops errors still implement error. Peel the wrapper to reach the metadata:
var e oops.OopsError
if errors.As(err, &e) {
code, dom := e.Code(), e.Domain()
tags, ctx := e.Tags(), e.Context()
trace := e.Stacktrace()
}
public := oops.GetPublic(err, "Something went wrong")Serialization is plug-and-play: %+v prints the stack, json.Marshal yields fields for log pipelines, and slog.Error(msg, slog.Any("error", err)) embeds everything into one record.
Carrying builders through middleware
Stash a builder in the request context and let handlers start from it:
b := oops.In("http").Request(r, false).Trace(r.Header.Get("X-Trace-Id"))
ctx := oops.WithBuilder(r.Context(), b)
return oops.FromContext(ctx).Tags("handler", "users").Errorf("something failed")Assertions, config, additional logger wiring: advanced patterns.
Best practices
- Attributes for variable data, static messages for grouping.
- Wrap once per layer; let the chain accumulate context.
Recoverat every goroutine boundary.Publicfor anything crossing into an HTTP response; keep internals out.- Give errors codes and owners so alerting can page the right team.
Cross-references
golang-error-handling— sentinel/wrapping idioms and the log-or-return rule.golang-observability— slog handlers, APM grouping, and trace propagation.golang-samber-slog— shippingoopserrors through a slog pipeline.golang-pkg-go-dev/golang-gopls— package facts and call-site navigation.

