errs

API

The errs package API: New, Wrap, Define, attributes and accessors.

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
  • New starts a fresh error, capturing the stack at the call site.
  • Errorf is New with fmt.Sprintf-style formatting, no attributes.
  • Wrap attaches a new message to an existing error, keeping the original as the cause; nil in, nil out, so errs.Wrap(err, "...") is safe to call unconditionally on a possibly-nil err.
  • Wrapf is Wrap with formatting.
  • NewPanic turns a recovered panic value into an *errs.Error whose stack points at the panic site, for use in a recover() 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

main.go
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)
    }
}
Copyright © 2026