API
The full errs surface: five constructors, four attribute functions, three accessors, and Define for naming a failure mode once and reusing it everywhere. Everything here works with plain errors.Is/errors.As, because *errs.Error wraps like any other Go error. See Overview for install and the two-message design principle this API is built around.
import "github.com/gp-system/errs"
Constructors
func New(msg string, attrs ...Attr) *Error
func Errorf(format string, args ...any) *Error
func Wrap(err error, msg string, attrs ...Attr) *Error
func Wrapf(err error, format string, args ...any) *Error
func NewPanic(recovered any, attrs ...Attr) *Error
Newstarts a fresh error, capturing the stack at the call site.ErrorfisNewwithfmt.Sprintf-style formatting, no attributes.Wrapattaches a new message to an existing error, keeping the original as the cause;nilin,nilout, soerrs.Wrap(err, "...")is safe to call unconditionally on a possibly-nilerr.WrapfisWrapwith formatting.NewPanicturns a recovered panic value into an*errs.Errorwhose stack points at the panic site, for use in arecover()handler.
if err := repo.Save(ctx, order); err != nil {
return errs.Wrap(err, "orders: save", errs.With("order_id", order.ID))
}
if err := validateQuantity(qty); err != nil {
return errs.Errorf("invalid quantity %d: %w", qty, err)
}
Attributes
Attr is a func(*Error), applied in order to any constructor:
type Attr func(*Error)
func Code(code string) Attr // machine-readable identifier, stable across message rewording
func Public(msg string) Attr // the only text a client is ever allowed to see
func Status(code int) Attr // the HTTP-shaped status this failure maps to
func With(key string, value any) Attr // structured metadata, attached at this wrap level
var ErrOutOfStock = errs.Define("out_of_stock",
errs.Public("The product is out of stock."),
errs.Status(http.StatusConflict))
err := ErrOutOfStock.New("orders: decrement stock", errs.With("product_id", "prod_42"))
Code, Public and Status are usually set once, on a Defined definition; With is the one attribute you reach for at every individual wrap site, to attach the metadata specific to that call (an ID, a count, a query name).
Accessors
func CodeOf(err error) string
func PublicOf(err error) string
func StatusOf(err error) int
Each walks the error chain (via errors.As) looking for the first *errs.Error that set the corresponding attribute, so it works the same whether the error you hand it is the *errs.Error itself or something that wraps one further up. StatusOf returns 0 when nothing in the chain set a status, so a renderer can fall back to 500 explicitly rather than guessing.
Introspection: Frames, FullFrames, Chain
type Frame struct {
File string
Line int
Function string
}
type Step struct {
Msg string
File string
Line int
Function string
Code string
Meta map[string]any
}
func Frames(err error) []Frame // the stack captured at the point of origin
func FullFrames(err error) []Frame // Frames, plus intermediate non-wrapping call frames
func Chain(err error) []Step // one Step per wrap level, root cause last
Frames is the call stack at the moment the error was first created (New, Errorf or the first Wrap in the chain); FullFrames additionally includes frames of ordinary function calls that neither created nor wrapped the error, giving the complete call path. Chain is the wrap history: one Step per Wrap/Wrapf call, each with its own message, wrap site and whatever With/Code was attached there, ending in the innermost cause. These three functions are what logging and Sentry build their structured output from; you rarely call them directly outside of that.
Stack semantics: captured at creation
The stack trace is captured once, at the New/Errorf/first-Wrap/NewPanic call that creates the *errs.Error, using runtime.Callers. Later Wrap calls on the same error add a Step to the chain but do not recapture the stack: the stack always points at the point of origin, not at whatever call site happens to log it. This matters in practice: if you captured the stack when you logged instead of when you created the error, a deeply wrapped error would point at the logging call, the same line, every time, regardless of where the failure actually started. Capturing at creation means the stack is meaningful the one time it matters, and free every other time the error is passed around, wrapped or checked with errors.Is.
errs.Define: named failure modes
type Definition struct { /* ... */ }
func Define(code string, attrs ...Attr) *Definition
func (d *Definition) New(msg string, attrs ...Attr) *Error
func (d *Definition) Newf(format string, args ...any) *Error
func (d *Definition) Wrap(err error, msg string, attrs ...Attr) *Error
func (d *Definition) Wrapf(err error, format string, args ...any) *Error
Define declares a reusable failure mode once, as a package-level var, the same way you'd declare a sentinel error with errors.New. It requires a Status attribute set to a valid 4xx or 5xx HTTP status code, and panics at init time if that's missing or out of range:
var ErrOutOfStock = errs.Define("out_of_stock",
errs.Public("The product is out of stock."),
errs.Status(http.StatusConflict)) // required, or Define panics
That panic-at-init behavior exists because a Definition without a valid status is a contract nobody can safely render, and the failure mode should be caught the moment the binary starts, not on the first request that happens to exercise it. A Definition's value is the sentinel: errors.Is(err, ErrOutOfStock) works directly against it, because every error .New/.Wrap produces from it carries a custom Is matching the definition. .New/.Newf/.Wrap/.Wrapf mirror the package-level constructors, pre-seeded with the definition's code, public message and status, so every occurrence of out_of_stock in the codebase renders identically without repeating those three attributes at every call site.
Standalone example
package main
import (
"errors"
"fmt"
"net/http"
"github.com/gp-system/errs"
)
var ErrNotFound = errs.Define("not_found",
errs.Public("The requested item does not exist."),
errs.Status(http.StatusNotFound))
func lookup(id string) (string, error) {
if id != "known" {
return "", ErrNotFound.New("lookup", errs.With("id", id))
}
return "value", nil
}
func main() {
_, err := lookup("missing")
fmt.Println(errors.Is(err, ErrNotFound)) // true
fmt.Println(errs.CodeOf(err)) // "not_found"
fmt.Println(errs.StatusOf(err)) // 404
fmt.Println(errs.PublicOf(err)) // "The requested item does not exist."
for _, step := range errs.Chain(err) {
fmt.Printf("%s (%s:%d)\n", step.Msg, step.File, step.Line)
}
}
Related pages
- errs: Overview: install, the two-message design.
- errs: Integration:
LogValue, Sentry'sStackTrace(), and a minimal HTTP mapper. - Error model: the full request-to-response pipeline this API feeds.
- httperr: Overview: rendering
errserrors to RFC 9457 responses.