olekukonko/errors
GitHub: olekukonko/errors
Stars: 28 | Forks: 3
# errors — production-grade error handling for Go
[](https://pkg.go.dev/github.com/olekukonko/errors)
[](https://goreportcard.com/report/github.com/olekukonko/errors)
[](LICENSE)
[](https://golang.org/dl/)
A feature-complete error handling library for Go. Fully compatible with `errors.Is`, `errors.As`, and `errors.Unwrap`. Optimised for high-throughput systems with object pooling, hybrid context storage, and inlining-immune stack capture.
## Contents
- [Installation](#installation)
- [Package overview](#package-overview)
- [Core — `errors`](#core--errors)
- [Creating errors](#creating-errors)
- [Stack traces](#stack-traces)
- [Context](#context)
- [Wrapping and chaining](#wrapping-and-chaining)
- [Sentinel errors](#sentinel-errors)
- [Type assertions — Is / As](#type-assertions--is--as)
- [Multi-error aggregation](#multi-error-aggregation)
- [Retry](#retry)
- [Chain execution](#chain-execution)
- [Channel utilities and streaming](#channel-utilities-and-streaming)
- [HTTP helpers](#http-helpers)
- [Concurrent group](#concurrent-group)
- [Inspect](#inspect)
- [slog integration](#slog-integration)
- [Pool management](#pool-management)
- [Management — `errmgr`](#management--errmgr)
- [Performance](#performance)
- [Migration guide](#migration-guide)
- [FAQ](#faq)
## Installation
go get github.com/olekukonko/errors@latest
Requires Go 1.21 or later.
## Package overview
| Package | Purpose |
|---|---|
| `errors` | Core error type, wrapping, context, stack traces, retry, chain, multi-error, channel utilities |
| `errmgr` | Parameterised error templates, occurrence monitoring, threshold alerting |
## Core — `errors`
### Creating errors
// Fast — no stack trace, 0 allocations with pooling
err := errors.New("connection failed")
// Formatted — full fmt verb support including %w
err := errors.Newf("user %s not found", "alice")
err := errors.Errorf("query failed: %w", cause) // alias of Newf
// With stack trace
err := errors.Trace("critical issue")
err := errors.Tracef("query %s failed: %w", query, cause)
// Named — useful for sentinel-style matching
err := errors.Named("AuthError")
// Standard library compatible
err := errors.Std("connection failed") // returns plain error
err := errors.Stdf("error %s", "detail") // formatted plain error
### Stack traces
// Capture at creation
err := errors.Trace("critical issue")
// Add to an existing error
err = err.WithStack()
// Read frames
for _, frame := range err.Stack() {
fmt.Println(frame) // "main.go:42 main.main"
}
// Lightweight version (file:line only, no function names)
for _, frame := range err.FastStack() {
fmt.Println(frame)
}
Stack capture is immune to compiler inlining — frames are collected from
the physical call stack and trimmed by slice arithmetic, not by skip count.
### Context
err := errors.New("processing failed").
With("user_id", "123").
With("attempt", 3).
With("retryable", true)
// Read back
ctx := errors.Context(err) // map[user_id:123 attempt:3 retryable:true]
// Check for a key
if err.HasContextKey("user_id") { ... }
// Variadic bulk attach
err.With("k1", v1, "k2", v2)
// Semantic helpers
err.WithCode(500)
err.WithCategory("network")
err.WithTimeout()
err.WithRetryable()
The first four context items are stored in a fixed-size array (no allocation).
Items beyond four spill to a map.
### Wrapping and chaining
lowErr := errors.New("connection timeout").With("server", "db01")
bizErr := errors.New("failed to load user").Wrap(lowErr)
apiErr := errors.Wrapf(bizErr, "request failed: %w", bizErr)
// Traverse
for i, e := range errors.UnwrapAll(apiErr) {
fmt.Printf("%d. %s\n", i+1, e)
}
// 1. request failed: ...
// 2. failed to load user
// 3. connection timeout
### Sentinel errors
`Const` creates a stable, pointer-comparable sentinel safe for package-level variables.
var (
ErrNotFound = errors.Const("not_found", "resource not found")
ErrForbidden = errors.Const("forbidden", "access denied")
)
// Match anywhere in a chain
if errors.Is(err, ErrNotFound) { ... }
// Add call-site context without losing the sentinel
err := ErrNotFound.With("user 42 not found")
errors.Is(err, ErrNotFound) // true — sentinel is the cause
// JSON and slog work automatically
b, _ := json.Marshal(ErrNotFound) // {"error":"resource not found","code":"not_found"}
slog.Error("lookup failed", "err", ErrNotFound)
### Type assertions — Is / As
// Is — checks identity or name match
err := errors.Named("AuthError")
wrapped := errors.Wrapf(err, "login failed")
errors.Is(wrapped, err) // true
// As — extract the first matching *Error from the chain
var target *errors.Error
if errors.As(wrapped, &target) {
fmt.Println(target.Name()) // "AuthError"
}
// Generic helpers (Go 1.18+)
if e, ok := errors.AsType[*MyError](err); ok { ... }
if errors.IsType[*MyError](err) { ... }
found, ok := errors.FindType(err, func(e *MyError) bool {
return e.Code() == 404
})
codes := errors.Map(err, func(e *MyError) int { return e.Code() })
errors.Filter[*MyError](err) // [] *MyError from chain
errors.FirstOfType[*MyError](err) // first *MyError
### Multi-error aggregation
// Basic
m := errors.NewMultiError()
m.Add(errors.New("name required"))
m.Add(errors.New("email invalid"))
fmt.Println(m.Count()) // 2
// With limits and sampling
m := errors.NewMultiError(
errors.WithLimit(100),
errors.WithSampling(10), // 10% sample rate
)
// Custom formatter
m := errors.NewMultiError(
errors.WithFormatter(func(errs []error) string {
return fmt.Sprintf("%d errors", len(errs))
}),
)
// Inspect
m.First() // first error
m.Last() // last error
m.Errors() // []error snapshot
m.Has() // bool
m.Single() // nil | first error | *MultiError
// Filter
networkErrs := m.Filter(func(e error) bool {
return strings.Contains(e.Error(), "network")
})
// Merge two MultiErrors
m.Merge(other)
// Join is a convenience that collapses errors to *MultiError or nil
err := errors.Join(err1, err2, err3)
### Retry
retry := errors.NewRetry(
errors.WithMaxAttempts(5),
errors.WithDelay(200*time.Millisecond),
errors.WithMaxDelay(2*time.Second),
errors.WithJitter(true),
errors.WithBackoff(errors.ExponentialBackoff{}),
errors.WithRetryIf(errors.IsRetryable),
errors.WithOnRetry(func(attempt int, err error) {
log.Printf("attempt %d: %v", attempt, err)
}),
)
err := retry.Execute(func() error {
return callExternalService()
})
// Generic version — preserves return value
result, err := errors.ExecuteReply[string](retry, func() (string, error) {
return fetchData()
})
// Context-aware
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
retry2 := retry.Transform(errors.WithContext(ctx))
err = retry2.Execute(fn)
// Backoff strategies
errors.ConstantBackoff{}
errors.LinearBackoff{}
errors.ExponentialBackoff{}
### Chain execution
Sequential steps with per-step retry, timeout, tagging, and optional steps.
chain := errors.NewChain(
errors.ChainWithTimeout(10*time.Second),
errors.ChainWithLogHandler(slog.Default().Handler()),
).
Step(validateInput).Tag("validation").
Step(verifyKYC).Tag("kyc").
Step(processPayment).Tag("billing").Code(402).
Retry(3, 100*time.Millisecond, errors.WithRetryIf(errors.IsRetryable)).
Step(sendNotification).Tag("notification").Optional()
if err := chain.Run(); err != nil {
errors.Inspect(err, os.Stderr)
}
// Run all steps, collect every error
if err := chain.RunAll(); err != nil {
errors.Inspect(err, os.Stderr)
}
`StepCtx` passes the chain-level context (with its deadline) to the step, so
blocking calls like HTTP or database queries respect the chain timeout:
chain.StepCtx(func(ctx context.Context) error {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
_, err := http.DefaultClient.Do(req)
return err
})
### Channel utilities and streaming
#### `<-chan error` utilities
These compose with the standard Go `(chan T, chan error)` idiom rather than replacing it.
// Drain — block until channel closes, collect into *MultiError
err := errors.Drain(errs)
// First — return first non-nil error; ctx for deadline only, caller owns cancel
err := errors.First(ctx, errs)
if err != nil {
cancel() // caller decides to stop siblings
}
// Collect — bounded sample; wraps ErrLimitReached when n is hit
err := errors.Collect(ctx, errs, 10)
if errors.Is(err, errors.ErrLimitReached) {
log.Warn("more than 10 errors — some dropped")
}
// Fan — merge multiple error channels; caller must drain or cancel to avoid leak
merged := errors.Fan(ctx, validateErrs, enrichErrs)
for err := range merged {
log.Println(err)
}
#### Stream — concurrent item processing
// Process items concurrently, collect all errors
s := errors.NewStream(ctx, urls, func(url string) error {
return fetch(url)
}, 8) // 8 workers; omit for len(items) workers
// Option A — block until done
if err := s.Wait(); err != nil {
errors.Inspect(err, os.Stderr)
}
// Option B — process errors as they arrive
s.Each(func(err error) {
log.Println(err)
})
// Stop early (drains channel to avoid goroutine leak)
s.Stop()
`Wait` and `Each` are mutually exclusive. Calling either a second time panics immediately.
### HTTP helpers
// Resolve HTTP status from an *Error's code
status := errors.HTTPStatusCode(err, http.StatusInternalServerError)
// Write HTTP error response
errors.HTTPError(w, err) // plain text, status from err.Code()
// With options
errors.HTTPError(w, err,
errors.WithFallbackCode(http.StatusBadGateway),
errors.WithBody(false), // header only
errors.WithBodyFunc(func(e error) string {
return fmt.Sprintf(`{"error":%q}`, e.Error())
}),
)
### Concurrent group
`Group` collects all errors from concurrent goroutines — unlike `errgroup` which stops at the first.
g := errors.NewGroup()
g.Go(func() error { return validateUser(id) })
g.Go(func() error { return validatePerms(id) })
if err := g.Wait(); err != nil {
// err is *MultiError containing every failure
errors.Inspect(err, os.Stderr)
}
// Context-aware
g := errors.NewGroup(
errors.GroupWithContext(ctx, true), // cancelOnFirst=true
errors.GroupWithLimit(50),
)
g.GoCtx(func(ctx context.Context) error {
return longRunningCheck(ctx)
})
_ = g.Wait()
### Inspect
// Default — writes to os.Stderr
errors.Inspect(err)
// Targeted output
var buf bytes.Buffer
errors.Inspect(err, &buf)
// Multiple destinations
errors.Inspect(err, os.Stderr, logFile)
// Options
errors.Inspect(err, os.Stderr,
errors.WithStackFrames(5),
errors.WithMaxDepth(20),
)
// *Error-specific convenience
errors.InspectError(err, os.Stderr)
`Inspect` handles `*Error`, `*MultiError`, and any stdlib error. It writes
to the supplied `io.Writer` values (merged via `io.MultiWriter`) and never
touches stdout.
### slog integration
Both `*Error` and `*Sentinel` implement `slog.LogValuer`:
slog.Error("request failed", "err", err)
// produces structured group: err.message, err.name, err.code, err.category, err.context, err.cause
slog.Error("lookup failed", "err", errors.ErrNotFound)
// produces: err.error="resource not found", err.code="not_found"
### Pool management
// Pre-warm (called automatically at init with 100 instances)
errors.WarmPool(1000)
errors.WarmStackPool(500)
// Tune global config
errors.Configure(errors.Config{
StackDepth: 32,
ContextSize: 4,
DisablePooling: false,
FilterInternal: true,
AutoFree: false, // opt-in GC-based pool return
})
// Explicit pool return (preferred)
err := errors.New("temp")
defer err.Free()
// Copy without affecting original
copied := err.Copy().With("extra", "data")
// Transform (non-destructive)
enriched := errors.Transform(err, func(e *errors.Error) {
e.WithCode(500).With("env", "prod").WithStack()
})
## Management — `errmgr`
### Parameterised error templates
// Define a reusable template
var ErrDBQuery = errmgr.Define("DBQuery", "database query failed: %s")
// Instantiate with arguments
err := ErrDBQuery("SELECT timed out")
fmt.Println(err) // "database query failed: SELECT timed out"
fmt.Println(err.Category()) // "database"
### Predefined errors
err := errmgr.ErrNotFound
fmt.Println(err.Code()) // 404
err := errmgr.ErrDBQuery("SELECT failed")
### Threshold monitoring
netErr := errmgr.Define("NetError", "network issue: %s")
monitor := errmgr.NewMonitor("NetError")
errmgr.SetThreshold("NetError", 3)
defer monitor.Close()
go func() {
for alert := range monitor.Alerts() {
fmt.Printf("alert: %s (count: %d)\n", alert, alert.Count())
}
}()
err := netErr("timeout")
err.Free()
Key design decisions:
- **Pool** — `New` and `Wrap` reuse `*Error` instances from `sync.Pool` (12 ns/op, 0 allocs).
- **Hybrid context** — up to 4 key-value pairs in a fixed array; overflow to map. Avoids heap allocation for the common case.
- **Stack capture** — `captureStack` is inlining-immune: it always starts from `runtime.Callers` frame 1 and trims by array slicing, so the compiler's inlining decisions never corrupt the skip count.
- **Pool capacity preservation** — the pool buffer is trimmed in-place (`copy(buf, buf[trimmed:n])`), not re-allocated. Prevents progressive capacity shrinkage under repeated `Free()` cycles.
- **`MarshalJSON`** — bytes are copied out of the pool buffer before returning it, eliminating the race between concurrent JSON serialisations.
- **`With()`** — the mutex is acquired once at entry, eliminating the TOCTOU race in the former optimistic read-then-lock path.
## Migration guide
### From standard library
// Before
err := fmt.Errorf("user %s not found: %w", username, cause)
// After — same output, plus context, code, and chain traversal
err := errors.Newf("user %s not found: %w", username, cause).
With("username", username).
WithCode(404)
### From `pkg/errors`
// Before
err := pkgerrors.Wrap(cause, "operation failed")
// After
err := errors.New("operation failed").Wrap(cause).WithStack()
### Stdlib `errors.Is` / `errors.As` compatibility
// Fully compatible — no changes needed
if errors.Is(err, io.EOF) { ... }
var target *errors.Error
if errors.As(err, &target) {
fmt.Println(target.Name())
}
## FAQ
**When should I use `Const` vs `Named`?**
`Const` — package-level sentinel for `errors.Is` matching. Returns the same pointer every call, so pointer equality works. `Named` — creates a new `*Error` instance each call; useful for structured errors with context but not for `==` comparison.
**When should I use `Const` vs `errmgr.Define`?**
`errors.Const("not_found", "resource not found")` creates a static sentinel. `errmgr.Define("DBQuery", "query failed: %s")` creates a parameterised factory — you call it with arguments to produce a new `*Error` each time.
**When should I call `Free()`?**
In hot paths where the error is short-lived and you want to return it to the pool immediately. For most application code, letting the GC handle it is fine. If `AutoFree` is enabled in `Config`, the GC returns the error automatically — but `defer err.Free()` is more predictable.
**Why does `First` not cancel the context?**
`context.Context` is immutable — only `context.WithCancel` produces a cancellable context. `First` accepts `ctx` for deadline support only. The pattern is: call `First`, then call `cancel()` yourself if you want to stop siblings.
**Why do `Each` and `Wait` on `Stream` panic on second call?**
Consuming the same channel twice silently splits errors between two callers. The panic surfaces the bug immediately rather than letting it produce subtly wrong results in production.
**How do I debug a deep error chain?**
errors.Inspect(err, os.Stderr, errors.WithMaxDepth(30), errors.WithStackFrames(10))
**How do I write to both stderr and a log file?**
errors.Inspect(err, os.Stderr, logFile) // io.MultiWriter internally
## License
MIT — see [LICENSE](LICENSE).
标签:EVTX分析