Overview
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
Publicattribute): 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.
| Layer | Responsibility |
|---|---|
errs | builds and wraps the error: stack, code, public message, status |
httperr | renders it to an RFC 9457 problem+json HTTP response |
| your own code | anything 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:
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"
}
Related pages
- 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
errserrors to HTTP. - Error model: the whole pipeline, end to end, with a worked example.