pleme-io's standard error model for Go. One concrete error type carries a
message, a cause chain, an optional machine code, and a severity — the Go
counterpart to the Rust fleet's anyhow + thiserror, with the severity ladder
from pleme-actions-shared folded in.
Pure standard library. No dependencies. Drop it into any module and it stays fully interoperable with
errors.Is,errors.As,errors.Unwrap, andfmt.Errorf("%w", …).
A single importable Go package (Biblioteca) providing one concrete Error
type plus a small, orthogonal API around it: New / Wrap for construction,
WithSeverity / WithCode options, SeverityOf / CodeOf for chain-walking
inspection, and Join for aggregation. It mirrors the pleme-io Rust fleet's
anyhow + thiserror ergonomics and folds in the pleme-actions-shared
severity ladder, while remaining a drop-in companion to the stdlib errors
package.
Three concerns recur in every fleet service, and the stdlib errors package
solves only the first:
| Concern | Rust analog | This package |
|---|---|---|
| Free-form context wrapping | anyhow .context(...) |
Wrap |
| Typed, inspectable sentinels + codes | thiserror |
New + WithCode |
| Operator-facing severity | pleme-actions-shared |
Severity + SeverityOf |
Rather than a parallel error hierarchy, everything is one type — Error — that
satisfies error and unwraps transparently. You keep using the stdlib errors
package for everything else.
go get github.com/pleme-io/errors-goimport errs "github.com/pleme-io/errors-go"Nix (build-verification check via the substrate go-library-flake):
nix build github:pleme-io/errors-go # runs `go build ./...` in the sandbox
nix run github:pleme-io/errors-go#check-all # full GSDS check surfaceWrap adds context while preserving the cause for errors.Is/errors.As. It
returns nil when given nil, so it is safe in a tail position:
func reconcile() error {
if err := loadConfig(); err != nil {
return errs.Wrap(err, "reconcile tenant config")
}
return nil
}A fully wrapped chain renders outermost-intent first:
serve request: reconcile: load tenant: not found
By default a wrap inherits the wrapped error's severity, so context-only
wrapping never silently downgrades a failure. Pass WithSeverity to re-classify.
Define sentinels with New; match them across any number of wraps with the
stdlib errors.Is:
var ErrNotFound = errs.New("resource not found", errs.WithCode("E_NOT_FOUND"))
err := errs.Wrap(errs.Wrap(ErrNotFound, "fetch from store"), "handle request")
errors.Is(err, ErrNotFound) // true, across both wraps
errs.CodeOf(err) // "E_NOT_FOUND" — first non-empty code, walking the chainRecover the typed value with errors.As:
var e *errs.Error
if errors.As(err, &e) {
_ = e.Severity()
_ = e.Code()
}The ladder mirrors pleme-actions-shared. Constants are prefixed
(SeverityNotice/SeverityWarning/SeverityError) — the marquee type is named
Error, so a bare Error const would collide with it, exactly the situation
log/slog resolves with LevelInfo/LevelWarn.
err := errs.New("token nearing expiry", errs.WithSeverity(errs.SeverityWarning))
switch errs.SeverityOf(err) {
case errs.SeverityNotice: // informational
case errs.SeverityWarning: // degraded, still operating
case errs.SeverityError: // failed (the default)
}SeverityOf walks the chain and defaults to SeverityError for any error
without severity metadata — including plain stdlib errors — so failures are loud
by default and a missing annotation is never read as benign.
Join mirrors errors.Join: nil inputs are dropped, the result is nil when all
inputs are nil, and errors.Is/errors.As reach every member. On top of that
it computes the aggregate's metadata: severity is the most severe member,
and the code is the first non-empty member code (argument order).
err := errs.Join(
errs.New("config A invalid", errs.WithSeverity(errs.SeverityNotice)),
errs.New("config B invalid", errs.WithSeverity(errs.SeverityWarning), errs.WithCode("E_B")),
)
errs.SeverityOf(err) // SeverityWarning (most severe member)
errs.CodeOf(err) // "E_B"The standard library's errors.Join also works on values from this package —
use Join when you want the aggregate's severity/code rolled up, and the
stdlib's when you don't.
| Symbol | Purpose |
|---|---|
type Error struct |
the one concrete error type; Unwrap() error |
type Severity int + SeverityNotice / SeverityWarning / SeverityError |
the ladder; String() |
DefaultSeverity |
SeverityError — the fallback for un-annotated errors |
New(msg string, opts ...Option) error |
leaf / sentinel constructor |
Wrap(err error, msg string, opts ...Option) error |
context wrap; nil in → nil out |
WithSeverity(Severity) Option, WithCode(string) Option |
constructor options |
SeverityOf(err error) Severity |
severity, walking the chain (default SeverityError) |
CodeOf(err error) string |
first non-empty code, walking the chain |
Join(errs ...error) error |
aggregate; rolls up severity (max) and code (first) |
None. errors-go is a pure-stdlib Biblioteca with no runtime configuration,
no environment variables, and no external dependencies — it is configured
entirely through its function arguments and the WithSeverity / WithCode
options at construction time.
Released via the pleme-io pull-model: a semver git tag (vX.Y.Z) is pushed and
proxy.golang.org fetches the module lazily — there is no artifact upload. Each
tag corresponds to a dated section in CHANGELOG.md.
Build and test locally:
go build ./...
go test -race -cover ./... # 100% statement coverage, race-clean
nix flake check # GSDS build-verification + app surfacePure stdlib, 100% statement coverage, race-clean.