Skip to content

pleme-io/errors-go

Repository files navigation

errors-go

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, and fmt.Errorf("%w", …).

What

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.

Why

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.

Install

go get github.com/pleme-io/errors-go
import 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 surface

Usage

Wrapping (anyhow)

Wrap 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.

Typed sentinels (thiserror)

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 chain

Recover the typed value with errors.As:

var e *errs.Error
if errors.As(err, &e) {
    _ = e.Severity()
    _ = e.Code()
}

Severity

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.

Aggregation

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.

Surface

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)

Configuration

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.

Release

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 surface

Pure stdlib, 100% statement coverage, race-clean.

About

pleme-io's standard error model for Go — one concrete error type carrying message, cause chain, optional machine code, and severity (the Go counterpart to the Rust anyhow + thiserror stack)

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors