golang-samber-oops

Structured error handling in Golang with samber/oops — error builders, stack traces, error codes, error context, error wrapping, error attributes, user-facing…

INSTALLATION
npx skills add https://github.com/samber/cc-skills-golang --skill golang-samber-oops
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

All oops errors use a fluent builder pattern:

err := oops.

    In("user-service").           // domain/feature

    Tags("database", "postgres").  // categorization

    Code("network_failure").       // machine-readable identifier

    User("user-123", "email", "foo@bar.com").  // user context

    With("query", query).          // custom attributes

    Errorf("failed to fetch user: %s", "timeout")

Terminal methods:

  • .Errorf(format, args...) — create a new error
  • .Wrap(err) — wrap an existing error
  • .Wrapf(err, format, args...) — wrap with a message
  • .Join(err1, err2, ...) — combine multiple errors
  • .Recover(fn) / .Recoverf(fn, format, args...) — convert panic to error

Error builder methods

Methods

Use case

.With("key", value)

Add custom key-value attribute (lazy func() any values supported)

.WithContext(ctx, "key1", "key2")

Extract values from Go context into attributes (lazy values supported)

.In("domain")

Set the feature/service/domain

.Tags("auth", "sql")

Add categorization tags (query with err.HasTag("tag"))

.Code("iam_authz_missing_permission")

Set machine-readable error identifier/slug

.Public("Could not fetch user.")

Set user-safe message (separate from technical details)

.Hint("Runbook: https://doc.acme.org/doc/abcd.md")

Add debugging hint for developers

.Owner("team/slack")

Identify responsible team/owner

.User(id, "k", "v")

Add user identifier and attributes

.Tenant(id, "k", "v")

Add tenant/organization context and attributes

.Trace(id)

Add trace / correlation ID (default: ULID)

.Span(id)

Add span ID representing a unit of work/operation (default: ULID)

.Time(t)

Override error timestamp (default: time.Now())

.Since(t)

Set duration based on time since t (exposed via err.Duration())

.Duration(d)

Set explicit error duration

.Request(req, includeBody)

Attach *http.Request (optionally including body)

.Response(res, includeBody)

Attach *http.Response (optionally including body)

oops.FromContext(ctx)

Start from an OopsErrorBuilder stored in a Go context

Common scenarios

Database/repository layer

func (r *UserRepository) FetchUser(id string) (*User, error) {

    query := "SELECT * FROM users WHERE id = $1"

    row, err := r.db.Query(query, id)

    if err != nil {

        return nil, oops.

            In("user-repository").

            Tags("database", "postgres").

            With("query", query).

            With("user_id", id).

            Wrapf(err, "failed to fetch user from database")

    }

    // ...

}

HTTP handler layer

func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {

    userID := getUserID(r)

    err := h.service.CreateUser(r.Context(), userID)

    if err != nil {

        err = oops.

            In("http-handler").

            Tags("endpoint", "/users").

            Request(r, false).

            User(userID).

            Wrapf(err, "create user failed")

        http.Error(w, oops.GetPublic(err, "Internal server error"), http.StatusInternalServerError)

        return

    }

    w.WriteHeader(http.StatusCreated)

}

Service layer with reusable builder

func (s *UserService) CreateOrder(ctx context.Context, req CreateOrderRequest) error {

    builder := oops.

        In("order-service").

        Tags("orders", "checkout").

        Tenant(req.TenantID, "plan", req.Plan).

        User(req.UserID, "email", req.UserEmail)

    product, err := s.catalog.GetProduct(ctx, req.ProductID)

    if err != nil {

        return builder.

            With("product_id", req.ProductID).

            Wrapf(err, "product lookup failed")

    }

    if product.Stock < req.Quantity {

        return builder.

            Code("insufficient_stock").

            Public("Not enough items in stock.").

            With("requested", req.Quantity).

            With("available", product.Stock).

            Errorf("insufficient stock for product %s", req.ProductID)

    }

    return nil

}

Error wrapping best practices

DO: Wrap directly, no nil check needed

// ✓ Good — Wrap returns nil if err is nil

return oops.Wrapf(err, "operation failed")

// ✗ Bad — unnecessary nil check

if err != nil {

    return oops.Wrapf(err, "operation failed")

}

return nil

DO: Add context at each layer

Each architectural layer SHOULD add context via Wrap/Wrapf — at least once per package boundary (not necessarily at every function call).

// ✓ Good — each layer adds relevant context

func Controller() error {

    return oops.In("controller").Trace(traceID).Wrapf(Service(), "user request failed")

}

func Service() error {

    return oops.In("service").With("op", "create_user").Wrapf(Repository(), "db operation failed")

}

func Repository() error {

    return oops.In("repository").Tags("database", "postgres").Errorf("connection timeout")

}

DO: Keep error messages low-cardinality

Error messages MUST be low-cardinality for APM aggregation. Interpolating variable data into the message breaks grouping in Datadog, Loki, Sentry.

// ✗ Bad — high-cardinality, breaks APM grouping

oops.Errorf("failed to process user %s in tenant %s", userID, tenantID)

// ✓ Good — static message + structured attributes

oops.With("user_id", userID).With("tenant_id", tenantID).Errorf("failed to process user")

Panic recovery

oops.Recover() MUST be used in goroutine boundaries. Convert panics to structured errors:

func ProcessData(data string) (err error) {

    return oops.

        In("data-processor").

        Code("panic_recovered").

        Hint("Check input data format and dependencies").

        With("input_data", data).

        Recover(func() {

            riskyOperation(data)

        })

}

Accessing error information

samber/oops errors implement the standard error interface. Access additional info:

if oopsErr, ok := err.(oops.OopsError); ok {

    fmt.Println("Code:", oopsErr.Code())

    fmt.Println("Domain:", oopsErr.Domain())

    fmt.Println("Tags:", oopsErr.Tags())

    fmt.Println("Context:", oopsErr.Context())

    fmt.Println("Stacktrace:", oopsErr.Stacktrace())

}

// Get public-facing message with fallback

publicMsg := oops.GetPublic(err, "Something went wrong")

Output formats

fmt.Printf("%+v\n", err)       // verbose with stack trace

bytes, _ := json.Marshal(err)  // JSON for logging

slog.Error(err.Error(), slog.Any("error", err))  // slog integration

Context propagation

Carry error context through Go contexts:

func middleware(next http.Handler) http.Handler {

    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

        builder := oops.

            In("http").

            Request(r, false).

            Trace(r.Header.Get("X-Trace-ID"))

        ctx := oops.WithBuilder(r.Context(), builder)

        next.ServeHTTP(w, r.WithContext(ctx))

    })

}

func handler(ctx context.Context) error {

    return oops.FromContext(ctx).Tags("handler", "users").Errorf("something failed")

}

For assertions, configuration, and additional logger examples, see Advanced patterns.

References

Cross-References

  • → See samber/cc-skills-golang@golang-error-handling skill for general error handling patterns
  • → See samber/cc-skills-golang@golang-observability skill for logger integration and structured logging
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card