errs

Integration

Wiring errs into logging, Sentry and plain net/http handlers.

errs builds errors; it deliberately does not know about slog, Sentry or HTTP. This page covers the three places something else picks up an *errs.Error and does something with it: the standard logger, Sentry, and a hand-rolled HTTP mapper for projects that want errs without pulling in httperr.

Log output

*errs.Error implements slog.LogValuer:

func (e *Error) LogValue() slog.Value

Passing an error to slog.Any("error", err) emits a structured group, not one flattened string: the internal message, the code, the public message, the Chain (one object per wrap level) and the Frames (the full call stack from the point of origin). This happens automatically, with no per-call-site formatting, anywhere an *errs.Error reaches a slog call:

slog.Error("request failed", slog.Any("error", err))
{
  "msg": "request failed",
  "error": {
    "msg": "shop: place order: orders: decrement stock: connection refused",
    "code": "shop_api_place_order_failed",
    "public": "Placing the order failed, please try again later.",
    "chain": [
      { "msg": "shop: place order", "file": "service.go", "line": 42, "function": "service.(*Service).PlaceOrder" },
      { "msg": "orders: decrement stock", "file": "orders.go", "line": 31, "function": "repository.(*Orders).DecrementStock", "meta": { "product_id": "prod_42" } },
      { "msg": "connection refused" }
    ],
    "stack": [
      { "file": "orders.go", "line": 31, "function": "repository.(*Orders).DecrementStock" },
      { "file": "service.go", "line": 42, "function": "service.(*Service).PlaceOrder" }
    ]
  }
}

One pitfall to avoid: LogValue only fires when slog actually receives an error-typed value carrying an *errs.Error. If you call err.Error() first and log the resulting string, you get exactly that: a flat string, no structure. Always pass the error itself (slog.Any("error", err)), never its rendered message. The Logging page covers the rest of the logging setup, the console/JSON handlers and how this structured group looks in each.

Sentry

*errs.Error exposes the one method sentry-go looks for via reflection when building an exception's stack frames:

func (e *Error) StackTrace() []uintptr

Any Sentry integration that accepts a plain error and checks for this method (as telemetry/sentryx does) gets an accurate stack with no extra wiring: no manual sentry.NewStacktrace() call, no frame-skipping arithmetic. The error's Code (when set, typically via errs.Define) doubles as a stable Sentry fingerprint: retitling the exception with the code before it's sent means one failure mode groups into one Sentry issue regardless of how the message text is worded at each wrap site, or how many different call sites produce it.

A minimal net/http mapper without the kit or httperr

httperr exists because that mapping, done properly (content negotiation, RFC 9457 shape, validation errors, dev-mode gating), is more than a few lines. But if you only want errs's stack/code/public-message model and don't want the extra dependency, the minimal version is genuinely small:

errmap/errmap.go
package errmap

import (
    "encoding/json"
    "errors"
    "log/slog"
    "net/http"

    "github.com/gp-system/errs"
)

func Write(w http.ResponseWriter, r *http.Request, err error) {
    status := errs.StatusOf(err)
    if status == 0 {
        status = http.StatusInternalServerError
    }

    detail := errs.PublicOf(err)
    if status >= 500 {
        slog.ErrorContext(r.Context(), "request failed", slog.Any("error", err))
        if detail == "" {
            detail = "Internal server error."
        }
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    _ = json.NewEncoder(w).Encode(map[string]any{
        "status": status,
        "detail": detail,
        "code":   errs.CodeOf(err),
    })
}
main.go
func placeOrder(w http.ResponseWriter, r *http.Request) {
    if err := svc.PlaceOrder(r.Context(), input); err != nil {
        errmap.Write(w, r, err)
        return
    }
    w.WriteHeader(http.StatusCreated)
}

This is deliberately not RFC 9457, has no validation-error shape and no dev-mode detail gating: it exists to show that errs imposes no framework on you, not as a recommended alternative to httperr in a real project. The moment you want the RFC 9457 shape, per-field validation errors or the Stack/Chain dev-mode extension members, reach for httperr: the exact same errs.StatusOf/PublicOf/CodeOf calls back this hand-rolled version, just with more of the surrounding machinery already built.

With the kit

A generated gpsystem project never calls the accessors above directly: httperr.WriteError does, wired into the generated server's error hooks via server. You write errs.Define/errs.Wrap in your services and repositories exactly as shown here and on the API page; from there, errshttperrslog/Sentry is the wiring Error model walks end to end.

  • errs: API: the full constructor/attribute/accessor reference.
  • httperr: Overview and Error responses: the RFC 9457 renderer built on top of this.
  • Logging: the console/JSON handlers LogValue output feeds.
  • Sentry: the full Sentry integration, StackTrace() and code-based fingerprinting.
Copyright © 2026