errs

Overview

Structured Go errors with stacks, codes and client-safe messages.

errs is a standalone, stdlib-only Go module (github.com/gp-system/errs): errors that carry a stack trace, a machine-readable code and a client-safe public message, alongside the usual wrapped internal message. It has zero third-party dependencies, works in any Go program (a CLI, a Lambda, a plain net/http service), and needs no other gp-system module to be useful. The gpsystem kit uses it for exactly one reason: it's the error type every service, repository and handler in a generated project builds and wraps, and it's the type httperr renders to HTTP. See Error model for how the two fit together across a whole request.

Install

go get github.com/gp-system/errs@v0.1.0 # the kit itself uses this tag
go get github.com/gp-system/errs@latest

Go 1.25+, and nothing else: errs imports only the standard library (errors, fmt, runtime, log/slog). Adding it to a project pulls in no transitive dependency at all.

Why two messages

Every error errs builds carries two texts, deliberately kept apart:

  • the internal message (Error()): the full detail, meant for you. It includes the wrap chain, so a failing query, a product ID, a downstream timeout, all show up here.
  • the public message (set with the Public attribute): the only text a client is ever allowed to see. When none is set, there simply isn't one to leak; nothing falls back to the internal message.

Keeping the two separate means a stray err.Error() written into a response can never leak internals: whatever renders the error (the minimal mapper below, or httperr) reads errs.PublicOf, never Error(). The status code travels the same way: it is set on the error at the point it's defined, not decided later in a handler's if branch. An errs error is, in this sense, a small API contract in itself: code, public message and status together describe one failure mode, independent of whatever eventually renders it.

What rendering you still need

errs builds and carries the error. It does not know what an HTTP response, a JSON body or a log line looks like: rendering is a separate concern, left to the layer on top, which turns errs.CodeOf/PublicOf/StatusOf into bytes on the wire.

LayerResponsibility
errsbuilds and wraps the error: stack, code, public message, status
httperrrenders it to an RFC 9457 problem+json HTTP response
your own codeanything else: a gRPC status, a CLI exit code, a queue-message envelope

If you're not on HTTP, or don't want the extra dependency, Integration shows a mapper small enough to write yourself, with errs alone, no httperr.

Standalone example

A plain Go program, no HTTP, no kit:

main.go
package main

import (
    "errors"
    "fmt"
    "net/http"

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

var ErrOutOfStock = errs.Define("out_of_stock",
    errs.Public("The product is out of stock."),
    errs.Status(http.StatusConflict))

func decrementStock(productID string, qty int) error {
    if qty > 3 {
        return ErrOutOfStock.New("decrement stock",
            errs.With("product_id", productID))
    }
    return nil
}

func placeOrder(productID string) error {
    if err := decrementStock(productID, 5); err != nil {
        if errors.Is(err, ErrOutOfStock) {
            return err // expected: pass through unchanged
        }
        return errs.Wrap(err, "place order")
    }
    return nil
}

func main() {
    err := placeOrder("prod_42")
    fmt.Println(err)                // internal message, for you
    fmt.Println(errs.PublicOf(err)) // "The product is out of stock.", safe for a client
    fmt.Println(errs.StatusOf(err)) // 409
    fmt.Println(errs.CodeOf(err))   // "out_of_stock"
}
  • errs: API: the full New/Wrap/Define/attributes/accessors walkthrough.
  • errs: Integration: logging, Sentry, and a minimal HTTP mapper without httperr.
  • httperr: Overview: the module that renders errs errors to HTTP.
  • Error model: the whole pipeline, end to end, with a worked example.
Copyright © 2026