Latest commit

History

93 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

errors — production-grade error handling for Go

Go ReferenceGo Report CardLicenseGo 1.21+

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

go get github.com/olekukonko/errors@latest

Requires Go 1.21 or later.


Package overview

PackagePurpose
errorsCore error type, wrapping, context, stack traces, retry, chain, multi-error, channel utilities
errmgrParameterised error templates, occurrence monitoring, threshold alerting

Core — errors

Creating errors

// Fast — no stack trace, 0 allocations with poolingerr:=errors.New("connection failed")
// Formatted — full fmt verb support including %werr:=errors.Newf("user %s not found", "alice")
err:=errors.Errorf("query failed: %w", cause) // alias of Newf// With stack traceerr:=errors.Trace("critical issue")
err:=errors.Tracef("query %s failed: %w", query, cause)
// Named — useful for sentinel-style matchingerr:=errors.Named("AuthError")
// Standard library compatibleerr:=errors.Std("connection failed") // returns plain errorerr:=errors.Stdf("error %s", "detail") // formatted plain error

Stack traces

// Capture at creationerr:=errors.Trace("critical issue")
// Add to an existing errorerr=err.WithStack()
// Read framesfor_, frame:=rangeerr.Stack() {
fmt.Println(frame) // "main.go:42 main.main"
}
// Lightweight version (file:line only, no function names)for_, frame:=rangeerr.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 backctx:=errors.Context(err) // map[user_id:123 attempt:3 retryable:true]// Check for a keyiferr.HasContextKey("user_id") { ... }
// Variadic bulk attacherr.With("k1", v1, "k2", v2)
// Semantic helperserr.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)
// Traversefori, e:=rangeerrors.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 chainiferrors.Is(err, ErrNotFound) { ... }
// Add call-site context without losing the sentinelerr:=ErrNotFound.With("user 42 not found")
errors.Is(err, ErrNotFound) // true — sentinel is the cause// JSON and slog work automaticallyb, _:=json.Marshal(ErrNotFound) // {"error":"resource not found","code":"not_found"}slog.Error("lookup failed", "err", ErrNotFound)

Const vs errmgr.Defineerrors.Const — static comparable value for errors.Is matching. errmgr.Define — parameterised factory that creates new *Error instances from a format template.

Type assertions — Is / As

// Is — checks identity or name matcherr:=errors.Named("AuthError")
wrapped:=errors.Wrapf(err, "login failed")
errors.Is(wrapped, err) // true// As — extract the first matching *Error from the chainvartarget*errors.Erroriferrors.As(wrapped, &target) {
fmt.Println(target.Name()) // "AuthError"
}
// Generic helpers (Go 1.18+)ife, ok:= errors.AsType[*MyError](err); ok { ... }
if errors.IsType[*MyError](err) { ... }
found, ok:=errors.FindType(err, func(e*MyError) bool {
returne.Code() ==404
})
codes:=errors.Map(err, func(e*MyError) int { returne.Code() })
errors.Filter[*MyError](err) // [] *MyError from chain
errors.FirstOfType[*MyError](err) // first *MyError

Is() string-equality note(*Error).Is falls back to string comparison as a convenience for matching stdlib errors by message. For strict identity matching use Const().

Multi-error aggregation

// Basicm:=errors.NewMultiError()
m.Add(errors.New("name required"))
m.Add(errors.New("email invalid"))
fmt.Println(m.Count()) // 2// With limits and samplingm:=errors.NewMultiError(
errors.WithLimit(100),
errors.WithSampling(10), // 10% sample rate
)
// Custom formatterm:=errors.NewMultiError(
errors.WithFormatter(func(errs []error) string {
returnfmt.Sprintf("%d errors", len(errs))
}),
)
// Inspectm.First() // first errorm.Last() // last errorm.Errors() // []error snapshotm.Has() // boolm.Single() // nil | first error | *MultiError// FilternetworkErrs:=m.Filter(func(eerror) bool {
returnstrings.Contains(e.Error(), "network")
})
// Merge two MultiErrorsm.Merge(other)
// Join is a convenience that collapses errors to *MultiError or nilerr:=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(attemptint, errerror) {
log.Printf("attempt %d: %v", attempt, err)
}),
)
err:=retry.Execute(func() error {
returncallExternalService()
})
// Generic version — preserves return valueresult, err:=errors.ExecuteReply[string](retry, func() (string, error) {
returnfetchData()
})
// Context-awarectx, cancel:=context.WithTimeout(context.Background(), 5*time.Second)
defercancel()
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()
iferr:=chain.Run(); err!=nil {
errors.Inspect(err, os.Stderr)
}
// Run all steps, collect every erroriferr:=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)
returnerr
})

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 *MultiErrorerr:=errors.Drain(errs)
// First — return first non-nil error; ctx for deadline only, caller owns cancelerr:=errors.First(ctx, errs)
iferr!=nil {
cancel() // caller decides to stop siblings
}
// Collect — bounded sample; wraps ErrLimitReached when n is hiterr:=errors.Collect(ctx, errs, 10)
iferrors.Is(err, errors.ErrLimitReached) {
log.Warn("more than 10 errors — some dropped")
}
// Fan — merge multiple error channels; caller must drain or cancel to avoid leakmerged:=errors.Fan(ctx, validateErrs, enrichErrs)
forerr:=rangemerged {
log.Println(err)
}

Stream — concurrent item processing

// Process items concurrently, collect all errorss:=errors.NewStream(ctx, urls, func(urlstring) error {
returnfetch(url)
}, 8) // 8 workers; omit for len(items) workers// Option A — block until doneiferr:=s.Wait(); err!=nil {
errors.Inspect(err, os.Stderr)
}
// Option B — process errors as they arrives.Each(func(errerror) {
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 codestatus:=errors.HTTPStatusCode(err, http.StatusInternalServerError)
// Write HTTP error responseerrors.HTTPError(w, err) // plain text, status from err.Code()// With optionserrors.HTTPError(w, err,
errors.WithFallbackCode(http.StatusBadGateway),
errors.WithBody(false), // header onlyerrors.WithBodyFunc(func(eerror) string {
returnfmt.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 { returnvalidateUser(id) })
g.Go(func() error { returnvalidatePerms(id) })
iferr:=g.Wait(); err!=nil {
// err is *MultiError containing every failureerrors.Inspect(err, os.Stderr)
}
// Context-awareg:=errors.NewGroup(
errors.GroupWithContext(ctx, true), // cancelOnFirst=trueerrors.GroupWithLimit(50),
)
g.GoCtx(func(ctx context.Context) error {
returnlongRunningCheck(ctx)
})
_=g.Wait()

Inspect

// Default — writes to os.Stderrerrors.Inspect(err)
// Targeted outputvarbuf bytes.Buffererrors.Inspect(err, &buf)
// Multiple destinationserrors.Inspect(err, os.Stderr, logFile)
// Optionserrors.Inspect(err, os.Stderr,
errors.WithStackFrames(5),
errors.WithMaxDepth(20),
)
// *Error-specific convenienceerrors.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.causeslog.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 configerrors.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")
defererr.Free()
// Copy without affecting originalcopied:=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 templatevarErrDBQuery=errmgr.Define("DBQuery", "database query failed: %s")
// Instantiate with argumentserr:=ErrDBQuery("SELECT timed out")
fmt.Println(err) // "database query failed: SELECT timed out"fmt.Println(err.Category()) // "database"

Predefined errors

err:=errmgr.ErrNotFoundfmt.Println(err.Code()) // 404err:=errmgr.ErrDBQuery("SELECT failed")

Threshold monitoring

netErr:=errmgr.Define("NetError", "network issue: %s")
monitor:=errmgr.NewMonitor("NetError")
errmgr.SetThreshold("NetError", 3)
defermonitor.Close()
gofunc() {
foralert:=rangemonitor.Alerts() {
fmt.Printf("alert: %s (count: %d)\n", alert, alert.Count())
}
}()
err:=netErr("timeout")
err.Free()

Key design decisions:

  • PoolNew 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 capturecaptureStack 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

// Beforeerr:=fmt.Errorf("user %s not found: %w", username, cause)
// After — same output, plus context, code, and chain traversalerr:=errors.Newf("user %s not found: %w", username, cause).
With("username", username).
WithCode(404)

From pkg/errors

// Beforeerr:=pkgerrors.Wrap(cause, "operation failed")
// Aftererr:=errors.New("operation failed").Wrap(cause).WithStack()

Stdlib errors.Is / errors.As compatibility

// Fully compatible — no changes needediferrors.Is(err, io.EOF) { ... }
vartarget*errors.Erroriferrors.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

Contributing

Fork → branch → commit → PR. Please include tests for new behaviour and run go test -count=10 -race ./... before opening a PR.

License

MIT — see LICENSE.

About

A production-grade error handling library for Go, offering zero-cost abstractions, stack traces, multi-error support, retries, and advanced monitoring through two complementary packages: errors (core) and errmgr (management).

Resources

Stars

29 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

93 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

errors — production-grade error handling for Go

Go ReferenceGo Report CardLicenseGo 1.21+

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

go get github.com/olekukonko/errors@latest

Requires Go 1.21 or later.


Package overview

PackagePurpose
errorsCore error type, wrapping, context, stack traces, retry, chain, multi-error, channel utilities
errmgrParameterised error templates, occurrence monitoring, threshold alerting

Core — errors

Creating errors

// Fast — no stack trace, 0 allocations with poolingerr:=errors.New("connection failed")
// Formatted — full fmt verb support including %werr:=errors.Newf("user %s not found", "alice")
err:=errors.Errorf("query failed: %w", cause) // alias of Newf// With stack traceerr:=errors.Trace("critical issue")
err:=errors.Tracef("query %s failed: %w", query, cause)
// Named — useful for sentinel-style matchingerr:=errors.Named("AuthError")
// Standard library compatibleerr:=errors.Std("connection failed") // returns plain errorerr:=errors.Stdf("error %s", "detail") // formatted plain error

Stack traces

// Capture at creationerr:=errors.Trace("critical issue")
// Add to an existing errorerr=err.WithStack()
// Read framesfor_, frame:=rangeerr.Stack() {
fmt.Println(frame) // "main.go:42 main.main"
}
// Lightweight version (file:line only, no function names)for_, frame:=rangeerr.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 backctx:=errors.Context(err) // map[user_id:123 attempt:3 retryable:true]// Check for a keyiferr.HasContextKey("user_id") { ... }
// Variadic bulk attacherr.With("k1", v1, "k2", v2)
// Semantic helperserr.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)
// Traversefori, e:=rangeerrors.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 chainiferrors.Is(err, ErrNotFound) { ... }
// Add call-site context without losing the sentinelerr:=ErrNotFound.With("user 42 not found")
errors.Is(err, ErrNotFound) // true — sentinel is the cause// JSON and slog work automaticallyb, _:=json.Marshal(ErrNotFound) // {"error":"resource not found","code":"not_found"}slog.Error("lookup failed", "err", ErrNotFound)

Const vs errmgr.Defineerrors.Const — static comparable value for errors.Is matching. errmgr.Define — parameterised factory that creates new *Error instances from a format template.

Type assertions — Is / As

// Is — checks identity or name matcherr:=errors.Named("AuthError")
wrapped:=errors.Wrapf(err, "login failed")
errors.Is(wrapped, err) // true// As — extract the first matching *Error from the chainvartarget*errors.Erroriferrors.As(wrapped, &target) {
fmt.Println(target.Name()) // "AuthError"
}
// Generic helpers (Go 1.18+)ife, ok:= errors.AsType[*MyError](err); ok { ... }
if errors.IsType[*MyError](err) { ... }
found, ok:=errors.FindType(err, func(e*MyError) bool {
returne.Code() ==404
})
codes:=errors.Map(err, func(e*MyError) int { returne.Code() })
errors.Filter[*MyError](err) // [] *MyError from chain
errors.FirstOfType[*MyError](err) // first *MyError

Is() string-equality note(*Error).Is falls back to string comparison as a convenience for matching stdlib errors by message. For strict identity matching use Const().

Multi-error aggregation

// Basicm:=errors.NewMultiError()
m.Add(errors.New("name required"))
m.Add(errors.New("email invalid"))
fmt.Println(m.Count()) // 2// With limits and samplingm:=errors.NewMultiError(
errors.WithLimit(100),
errors.WithSampling(10), // 10% sample rate
)
// Custom formatterm:=errors.NewMultiError(
errors.WithFormatter(func(errs []error) string {
returnfmt.Sprintf("%d errors", len(errs))
}),
)
// Inspectm.First() // first errorm.Last() // last errorm.Errors() // []error snapshotm.Has() // boolm.Single() // nil | first error | *MultiError// FilternetworkErrs:=m.Filter(func(eerror) bool {
returnstrings.Contains(e.Error(), "network")
})
// Merge two MultiErrorsm.Merge(other)
// Join is a convenience that collapses errors to *MultiError or nilerr:=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(attemptint, errerror) {
log.Printf("attempt %d: %v", attempt, err)
}),
)
err:=retry.Execute(func() error {
returncallExternalService()
})
// Generic version — preserves return valueresult, err:=errors.ExecuteReply[string](retry, func() (string, error) {
returnfetchData()
})
// Context-awarectx, cancel:=context.WithTimeout(context.Background(), 5*time.Second)
defercancel()
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()
iferr:=chain.Run(); err!=nil {
errors.Inspect(err, os.Stderr)
}
// Run all steps, collect every erroriferr:=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)
returnerr
})

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 *MultiErrorerr:=errors.Drain(errs)
// First — return first non-nil error; ctx for deadline only, caller owns cancelerr:=errors.First(ctx, errs)
iferr!=nil {
cancel() // caller decides to stop siblings
}
// Collect — bounded sample; wraps ErrLimitReached when n is hiterr:=errors.Collect(ctx, errs, 10)
iferrors.Is(err, errors.ErrLimitReached) {
log.Warn("more than 10 errors — some dropped")
}
// Fan — merge multiple error channels; caller must drain or cancel to avoid leakmerged:=errors.Fan(ctx, validateErrs, enrichErrs)
forerr:=rangemerged {
log.Println(err)
}

Stream — concurrent item processing

// Process items concurrently, collect all errorss:=errors.NewStream(ctx, urls, func(urlstring) error {
returnfetch(url)
}, 8) // 8 workers; omit for len(items) workers// Option A — block until doneiferr:=s.Wait(); err!=nil {
errors.Inspect(err, os.Stderr)
}
// Option B — process errors as they arrives.Each(func(errerror) {
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 codestatus:=errors.HTTPStatusCode(err, http.StatusInternalServerError)
// Write HTTP error responseerrors.HTTPError(w, err) // plain text, status from err.Code()// With optionserrors.HTTPError(w, err,
errors.WithFallbackCode(http.StatusBadGateway),
errors.WithBody(false), // header onlyerrors.WithBodyFunc(func(eerror) string {
returnfmt.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 { returnvalidateUser(id) })
g.Go(func() error { returnvalidatePerms(id) })
iferr:=g.Wait(); err!=nil {
// err is *MultiError containing every failureerrors.Inspect(err, os.Stderr)
}
// Context-awareg:=errors.NewGroup(
errors.GroupWithContext(ctx, true), // cancelOnFirst=trueerrors.GroupWithLimit(50),
)
g.GoCtx(func(ctx context.Context) error {
returnlongRunningCheck(ctx)
})
_=g.Wait()

Inspect

// Default — writes to os.Stderrerrors.Inspect(err)
// Targeted outputvarbuf bytes.Buffererrors.Inspect(err, &buf)
// Multiple destinationserrors.Inspect(err, os.Stderr, logFile)
// Optionserrors.Inspect(err, os.Stderr,
errors.WithStackFrames(5),
errors.WithMaxDepth(20),
)
// *Error-specific convenienceerrors.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.causeslog.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 configerrors.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")
defererr.Free()
// Copy without affecting originalcopied:=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 templatevarErrDBQuery=errmgr.Define("DBQuery", "database query failed: %s")
// Instantiate with argumentserr:=ErrDBQuery("SELECT timed out")
fmt.Println(err) // "database query failed: SELECT timed out"fmt.Println(err.Category()) // "database"

Predefined errors

err:=errmgr.ErrNotFoundfmt.Println(err.Code()) // 404err:=errmgr.ErrDBQuery("SELECT failed")

Threshold monitoring

netErr:=errmgr.Define("NetError", "network issue: %s")
monitor:=errmgr.NewMonitor("NetError")
errmgr.SetThreshold("NetError", 3)
defermonitor.Close()
gofunc() {
foralert:=rangemonitor.Alerts() {
fmt.Printf("alert: %s (count: %d)\n", alert, alert.Count())
}
}()
err:=netErr("timeout")
err.Free()

Key design decisions:

  • PoolNew 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 capturecaptureStack 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

// Beforeerr:=fmt.Errorf("user %s not found: %w", username, cause)
// After — same output, plus context, code, and chain traversalerr:=errors.Newf("user %s not found: %w", username, cause).
With("username", username).
WithCode(404)

From pkg/errors

// Beforeerr:=pkgerrors.Wrap(cause, "operation failed")
// Aftererr:=errors.New("operation failed").Wrap(cause).WithStack()

Stdlib errors.Is / errors.As compatibility

// Fully compatible — no changes needediferrors.Is(err, io.EOF) { ... }
vartarget*errors.Erroriferrors.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

Contributing

Fork → branch → commit → PR. Please include tests for new behaviour and run go test -count=10 -race ./... before opening a PR.

License

MIT — see LICENSE.

About

A production-grade error handling library for Go, offering zero-cost abstractions, stack traces, multi-error support, retries, and advanced monitoring through two complementary packages: errors (core) and errmgr (management).

Resources

Stars

29 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

93 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

errors — production-grade error handling for Go

Go ReferenceGo Report CardLicenseGo 1.21+

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

go get github.com/olekukonko/errors@latest

Requires Go 1.21 or later.


Package overview

PackagePurpose
errorsCore error type, wrapping, context, stack traces, retry, chain, multi-error, channel utilities
errmgrParameterised error templates, occurrence monitoring, threshold alerting

Core — errors

Creating errors

// Fast — no stack trace, 0 allocations with poolingerr:=errors.New("connection failed")
// Formatted — full fmt verb support including %werr:=errors.Newf("user %s not found", "alice")
err:=errors.Errorf("query failed: %w", cause) // alias of Newf// With stack traceerr:=errors.Trace("critical issue")
err:=errors.Tracef("query %s failed: %w", query, cause)
// Named — useful for sentinel-style matchingerr:=errors.Named("AuthError")
// Standard library compatibleerr:=errors.Std("connection failed") // returns plain errorerr:=errors.Stdf("error %s", "detail") // formatted plain error

Stack traces

// Capture at creationerr:=errors.Trace("critical issue")
// Add to an existing errorerr=err.WithStack()
// Read framesfor_, frame:=rangeerr.Stack() {
fmt.Println(frame) // "main.go:42 main.main"
}
// Lightweight version (file:line only, no function names)for_, frame:=rangeerr.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 backctx:=errors.Context(err) // map[user_id:123 attempt:3 retryable:true]// Check for a keyiferr.HasContextKey("user_id") { ... }
// Variadic bulk attacherr.With("k1", v1, "k2", v2)
// Semantic helperserr.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)
// Traversefori, e:=rangeerrors.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 chainiferrors.Is(err, ErrNotFound) { ... }
// Add call-site context without losing the sentinelerr:=ErrNotFound.With("user 42 not found")
errors.Is(err, ErrNotFound) // true — sentinel is the cause// JSON and slog work automaticallyb, _:=json.Marshal(ErrNotFound) // {"error":"resource not found","code":"not_found"}slog.Error("lookup failed", "err", ErrNotFound)

Const vs errmgr.Defineerrors.Const — static comparable value for errors.Is matching. errmgr.Define — parameterised factory that creates new *Error instances from a format template.

Type assertions — Is / As

// Is — checks identity or name matcherr:=errors.Named("AuthError")
wrapped:=errors.Wrapf(err, "login failed")
errors.Is(wrapped, err) // true// As — extract the first matching *Error from the chainvartarget*errors.Erroriferrors.As(wrapped, &target) {
fmt.Println(target.Name()) // "AuthError"
}
// Generic helpers (Go 1.18+)ife, ok:= errors.AsType[*MyError](err); ok { ... }
if errors.IsType[*MyError](err) { ... }
found, ok:=errors.FindType(err, func(e*MyError) bool {
returne.Code() ==404
})
codes:=errors.Map(err, func(e*MyError) int { returne.Code() })
errors.Filter[*MyError](err) // [] *MyError from chain
errors.FirstOfType[*MyError](err) // first *MyError

Is() string-equality note(*Error).Is falls back to string comparison as a convenience for matching stdlib errors by message. For strict identity matching use Const().

Multi-error aggregation

// Basicm:=errors.NewMultiError()
m.Add(errors.New("name required"))
m.Add(errors.New("email invalid"))
fmt.Println(m.Count()) // 2// With limits and samplingm:=errors.NewMultiError(
errors.WithLimit(100),
errors.WithSampling(10), // 10% sample rate
)
// Custom formatterm:=errors.NewMultiError(
errors.WithFormatter(func(errs []error) string {
returnfmt.Sprintf("%d errors", len(errs))
}),
)
// Inspectm.First() // first errorm.Last() // last errorm.Errors() // []error snapshotm.Has() // boolm.Single() // nil | first error | *MultiError// FilternetworkErrs:=m.Filter(func(eerror) bool {
returnstrings.Contains(e.Error(), "network")
})
// Merge two MultiErrorsm.Merge(other)
// Join is a convenience that collapses errors to *MultiError or nilerr:=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(attemptint, errerror) {
log.Printf("attempt %d: %v", attempt, err)
}),
)
err:=retry.Execute(func() error {
returncallExternalService()
})
// Generic version — preserves return valueresult, err:=errors.ExecuteReply[string](retry, func() (string, error) {
returnfetchData()
})
// Context-awarectx, cancel:=context.WithTimeout(context.Background(), 5*time.Second)
defercancel()
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()
iferr:=chain.Run(); err!=nil {
errors.Inspect(err, os.Stderr)
}
// Run all steps, collect every erroriferr:=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)
returnerr
})

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 *MultiErrorerr:=errors.Drain(errs)
// First — return first non-nil error; ctx for deadline only, caller owns cancelerr:=errors.First(ctx, errs)
iferr!=nil {
cancel() // caller decides to stop siblings
}
// Collect — bounded sample; wraps ErrLimitReached when n is hiterr:=errors.Collect(ctx, errs, 10)
iferrors.Is(err, errors.ErrLimitReached) {
log.Warn("more than 10 errors — some dropped")
}
// Fan — merge multiple error channels; caller must drain or cancel to avoid leakmerged:=errors.Fan(ctx, validateErrs, enrichErrs)
forerr:=rangemerged {
log.Println(err)
}

Stream — concurrent item processing

// Process items concurrently, collect all errorss:=errors.NewStream(ctx, urls, func(urlstring) error {
returnfetch(url)
}, 8) // 8 workers; omit for len(items) workers// Option A — block until doneiferr:=s.Wait(); err!=nil {
errors.Inspect(err, os.Stderr)
}
// Option B — process errors as they arrives.Each(func(errerror) {
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 codestatus:=errors.HTTPStatusCode(err, http.StatusInternalServerError)
// Write HTTP error responseerrors.HTTPError(w, err) // plain text, status from err.Code()// With optionserrors.HTTPError(w, err,
errors.WithFallbackCode(http.StatusBadGateway),
errors.WithBody(false), // header onlyerrors.WithBodyFunc(func(eerror) string {
returnfmt.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 { returnvalidateUser(id) })
g.Go(func() error { returnvalidatePerms(id) })
iferr:=g.Wait(); err!=nil {
// err is *MultiError containing every failureerrors.Inspect(err, os.Stderr)
}
// Context-awareg:=errors.NewGroup(
errors.GroupWithContext(ctx, true), // cancelOnFirst=trueerrors.GroupWithLimit(50),
)
g.GoCtx(func(ctx context.Context) error {
returnlongRunningCheck(ctx)
})
_=g.Wait()

Inspect

// Default — writes to os.Stderrerrors.Inspect(err)
// Targeted outputvarbuf bytes.Buffererrors.Inspect(err, &buf)
// Multiple destinationserrors.Inspect(err, os.Stderr, logFile)
// Optionserrors.Inspect(err, os.Stderr,
errors.WithStackFrames(5),
errors.WithMaxDepth(20),
)
// *Error-specific convenienceerrors.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.causeslog.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 configerrors.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")
defererr.Free()
// Copy without affecting originalcopied:=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 templatevarErrDBQuery=errmgr.Define("DBQuery", "database query failed: %s")
// Instantiate with argumentserr:=ErrDBQuery("SELECT timed out")
fmt.Println(err) // "database query failed: SELECT timed out"fmt.Println(err.Category()) // "database"

Predefined errors

err:=errmgr.ErrNotFoundfmt.Println(err.Code()) // 404err:=errmgr.ErrDBQuery("SELECT failed")

Threshold monitoring

netErr:=errmgr.Define("NetError", "network issue: %s")
monitor:=errmgr.NewMonitor("NetError")
errmgr.SetThreshold("NetError", 3)
defermonitor.Close()
gofunc() {
foralert:=rangemonitor.Alerts() {
fmt.Printf("alert: %s (count: %d)\n", alert, alert.Count())
}
}()
err:=netErr("timeout")
err.Free()

Key design decisions:

  • PoolNew 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 capturecaptureStack 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

// Beforeerr:=fmt.Errorf("user %s not found: %w", username, cause)
// After — same output, plus context, code, and chain traversalerr:=errors.Newf("user %s not found: %w", username, cause).
With("username", username).
WithCode(404)

From pkg/errors

// Beforeerr:=pkgerrors.Wrap(cause, "operation failed")
// Aftererr:=errors.New("operation failed").Wrap(cause).WithStack()

Stdlib errors.Is / errors.As compatibility

// Fully compatible — no changes needediferrors.Is(err, io.EOF) { ... }
vartarget*errors.Erroriferrors.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

Contributing

Fork → branch → commit → PR. Please include tests for new behaviour and run go test -count=10 -race ./... before opening a PR.

License

MIT — see LICENSE.

About

A production-grade error handling library for Go, offering zero-cost abstractions, stack traces, multi-error support, retries, and advanced monitoring through two complementary packages: errors (core) and errmgr (management).

Resources

Stars

29 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

93 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

errors — production-grade error handling for Go

Go ReferenceGo Report CardLicenseGo 1.21+

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

go get github.com/olekukonko/errors@latest

Requires Go 1.21 or later.


Package overview

PackagePurpose
errorsCore error type, wrapping, context, stack traces, retry, chain, multi-error, channel utilities
errmgrParameterised error templates, occurrence monitoring, threshold alerting

Core — errors

Creating errors

// Fast — no stack trace, 0 allocations with poolingerr:=errors.New("connection failed")
// Formatted — full fmt verb support including %werr:=errors.Newf("user %s not found", "alice")
err:=errors.Errorf("query failed: %w", cause) // alias of Newf// With stack traceerr:=errors.Trace("critical issue")
err:=errors.Tracef("query %s failed: %w", query, cause)
// Named — useful for sentinel-style matchingerr:=errors.Named("AuthError")
// Standard library compatibleerr:=errors.Std("connection failed") // returns plain errorerr:=errors.Stdf("error %s", "detail") // formatted plain error

Stack traces

// Capture at creationerr:=errors.Trace("critical issue")
// Add to an existing errorerr=err.WithStack()
// Read framesfor_, frame:=rangeerr.Stack() {
fmt.Println(frame) // "main.go:42 main.main"
}
// Lightweight version (file:line only, no function names)for_, frame:=rangeerr.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 backctx:=errors.Context(err) // map[user_id:123 attempt:3 retryable:true]// Check for a keyiferr.HasContextKey("user_id") { ... }
// Variadic bulk attacherr.With("k1", v1, "k2", v2)
// Semantic helperserr.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)
// Traversefori, e:=rangeerrors.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 chainiferrors.Is(err, ErrNotFound) { ... }
// Add call-site context without losing the sentinelerr:=ErrNotFound.With("user 42 not found")
errors.Is(err, ErrNotFound) // true — sentinel is the cause// JSON and slog work automaticallyb, _:=json.Marshal(ErrNotFound) // {"error":"resource not found","code":"not_found"}slog.Error("lookup failed", "err", ErrNotFound)

Const vs errmgr.Defineerrors.Const — static comparable value for errors.Is matching. errmgr.Define — parameterised factory that creates new *Error instances from a format template.

Type assertions — Is / As

// Is — checks identity or name matcherr:=errors.Named("AuthError")
wrapped:=errors.Wrapf(err, "login failed")
errors.Is(wrapped, err) // true// As — extract the first matching *Error from the chainvartarget*errors.Erroriferrors.As(wrapped, &target) {
fmt.Println(target.Name()) // "AuthError"
}
// Generic helpers (Go 1.18+)ife, ok:= errors.AsType[*MyError](err); ok { ... }
if errors.IsType[*MyError](err) { ... }
found, ok:=errors.FindType(err, func(e*MyError) bool {
returne.Code() ==404
})
codes:=errors.Map(err, func(e*MyError) int { returne.Code() })
errors.Filter[*MyError](err) // [] *MyError from chain
errors.FirstOfType[*MyError](err) // first *MyError

Is() string-equality note(*Error).Is falls back to string comparison as a convenience for matching stdlib errors by message. For strict identity matching use Const().

Multi-error aggregation

// Basicm:=errors.NewMultiError()
m.Add(errors.New("name required"))
m.Add(errors.New("email invalid"))
fmt.Println(m.Count()) // 2// With limits and samplingm:=errors.NewMultiError(
errors.WithLimit(100),
errors.WithSampling(10), // 10% sample rate
)
// Custom formatterm:=errors.NewMultiError(
errors.WithFormatter(func(errs []error) string {
returnfmt.Sprintf("%d errors", len(errs))
}),
)
// Inspectm.First() // first errorm.Last() // last errorm.Errors() // []error snapshotm.Has() // boolm.Single() // nil | first error | *MultiError// FilternetworkErrs:=m.Filter(func(eerror) bool {
returnstrings.Contains(e.Error(), "network")
})
// Merge two MultiErrorsm.Merge(other)
// Join is a convenience that collapses errors to *MultiError or nilerr:=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(attemptint, errerror) {
log.Printf("attempt %d: %v", attempt, err)
}),
)
err:=retry.Execute(func() error {
returncallExternalService()
})
// Generic version — preserves return valueresult, err:=errors.ExecuteReply[string](retry, func() (string, error) {
returnfetchData()
})
// Context-awarectx, cancel:=context.WithTimeout(context.Background(), 5*time.Second)
defercancel()
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()
iferr:=chain.Run(); err!=nil {
errors.Inspect(err, os.Stderr)
}
// Run all steps, collect every erroriferr:=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)
returnerr
})

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 *MultiErrorerr:=errors.Drain(errs)
// First — return first non-nil error; ctx for deadline only, caller owns cancelerr:=errors.First(ctx, errs)
iferr!=nil {
cancel() // caller decides to stop siblings
}
// Collect — bounded sample; wraps ErrLimitReached when n is hiterr:=errors.Collect(ctx, errs, 10)
iferrors.Is(err, errors.ErrLimitReached) {
log.Warn("more than 10 errors — some dropped")
}
// Fan — merge multiple error channels; caller must drain or cancel to avoid leakmerged:=errors.Fan(ctx, validateErrs, enrichErrs)
forerr:=rangemerged {
log.Println(err)
}

Stream — concurrent item processing

// Process items concurrently, collect all errorss:=errors.NewStream(ctx, urls, func(urlstring) error {
returnfetch(url)
}, 8) // 8 workers; omit for len(items) workers// Option A — block until doneiferr:=s.Wait(); err!=nil {
errors.Inspect(err, os.Stderr)
}
// Option B — process errors as they arrives.Each(func(errerror) {
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 codestatus:=errors.HTTPStatusCode(err, http.StatusInternalServerError)
// Write HTTP error responseerrors.HTTPError(w, err) // plain text, status from err.Code()// With optionserrors.HTTPError(w, err,
errors.WithFallbackCode(http.StatusBadGateway),
errors.WithBody(false), // header onlyerrors.WithBodyFunc(func(eerror) string {
returnfmt.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 { returnvalidateUser(id) })
g.Go(func() error { returnvalidatePerms(id) })
iferr:=g.Wait(); err!=nil {
// err is *MultiError containing every failureerrors.Inspect(err, os.Stderr)
}
// Context-awareg:=errors.NewGroup(
errors.GroupWithContext(ctx, true), // cancelOnFirst=trueerrors.GroupWithLimit(50),
)
g.GoCtx(func(ctx context.Context) error {
returnlongRunningCheck(ctx)
})
_=g.Wait()

Inspect

// Default — writes to os.Stderrerrors.Inspect(err)
// Targeted outputvarbuf bytes.Buffererrors.Inspect(err, &buf)
// Multiple destinationserrors.Inspect(err, os.Stderr, logFile)
// Optionserrors.Inspect(err, os.Stderr,
errors.WithStackFrames(5),
errors.WithMaxDepth(20),
)
// *Error-specific convenienceerrors.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.causeslog.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 configerrors.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")
defererr.Free()
// Copy without affecting originalcopied:=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 templatevarErrDBQuery=errmgr.Define("DBQuery", "database query failed: %s")
// Instantiate with argumentserr:=ErrDBQuery("SELECT timed out")
fmt.Println(err) // "database query failed: SELECT timed out"fmt.Println(err.Category()) // "database"

Predefined errors

err:=errmgr.ErrNotFoundfmt.Println(err.Code()) // 404err:=errmgr.ErrDBQuery("SELECT failed")

Threshold monitoring

netErr:=errmgr.Define("NetError", "network issue: %s")
monitor:=errmgr.NewMonitor("NetError")
errmgr.SetThreshold("NetError", 3)
defermonitor.Close()
gofunc() {
foralert:=rangemonitor.Alerts() {
fmt.Printf("alert: %s (count: %d)\n", alert, alert.Count())
}
}()
err:=netErr("timeout")
err.Free()

Key design decisions:

  • PoolNew 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 capturecaptureStack 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

// Beforeerr:=fmt.Errorf("user %s not found: %w", username, cause)
// After — same output, plus context, code, and chain traversalerr:=errors.Newf("user %s not found: %w", username, cause).
With("username", username).
WithCode(404)

From pkg/errors

// Beforeerr:=pkgerrors.Wrap(cause, "operation failed")
// Aftererr:=errors.New("operation failed").Wrap(cause).WithStack()

Stdlib errors.Is / errors.As compatibility

// Fully compatible — no changes needediferrors.Is(err, io.EOF) { ... }
vartarget*errors.Erroriferrors.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

Contributing

Fork → branch → commit → PR. Please include tests for new behaviour and run go test -count=10 -race ./... before opening a PR.

License

MIT — see LICENSE.

About

A production-grade error handling library for Go, offering zero-cost abstractions, stack traces, multi-error support, retries, and advanced monitoring through two complementary packages: errors (core) and errmgr (management).

Resources

Stars

29 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

93 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

errors — production-grade error handling for Go

Go ReferenceGo Report CardLicenseGo 1.21+

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

go get github.com/olekukonko/errors@latest

Requires Go 1.21 or later.


Package overview

PackagePurpose
errorsCore error type, wrapping, context, stack traces, retry, chain, multi-error, channel utilities
errmgrParameterised error templates, occurrence monitoring, threshold alerting

Core — errors

Creating errors

// Fast — no stack trace, 0 allocations with poolingerr:=errors.New("connection failed")
// Formatted — full fmt verb support including %werr:=errors.Newf("user %s not found", "alice")
err:=errors.Errorf("query failed: %w", cause) // alias of Newf// With stack traceerr:=errors.Trace("critical issue")
err:=errors.Tracef("query %s failed: %w", query, cause)
// Named — useful for sentinel-style matchingerr:=errors.Named("AuthError")
// Standard library compatibleerr:=errors.Std("connection failed") // returns plain errorerr:=errors.Stdf("error %s", "detail") // formatted plain error

Stack traces

// Capture at creationerr:=errors.Trace("critical issue")
// Add to an existing errorerr=err.WithStack()
// Read framesfor_, frame:=rangeerr.Stack() {
fmt.Println(frame) // "main.go:42 main.main"
}
// Lightweight version (file:line only, no function names)for_, frame:=rangeerr.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 backctx:=errors.Context(err) // map[user_id:123 attempt:3 retryable:true]// Check for a keyiferr.HasContextKey("user_id") { ... }
// Variadic bulk attacherr.With("k1", v1, "k2", v2)
// Semantic helperserr.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)
// Traversefori, e:=rangeerrors.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 chainiferrors.Is(err, ErrNotFound) { ... }
// Add call-site context without losing the sentinelerr:=ErrNotFound.With("user 42 not found")
errors.Is(err, ErrNotFound) // true — sentinel is the cause// JSON and slog work automaticallyb, _:=json.Marshal(ErrNotFound) // {"error":"resource not found","code":"not_found"}slog.Error("lookup failed", "err", ErrNotFound)

Const vs errmgr.Defineerrors.Const — static comparable value for errors.Is matching. errmgr.Define — parameterised factory that creates new *Error instances from a format template.

Type assertions — Is / As

// Is — checks identity or name matcherr:=errors.Named("AuthError")
wrapped:=errors.Wrapf(err, "login failed")
errors.Is(wrapped, err) // true// As — extract the first matching *Error from the chainvartarget*errors.Erroriferrors.As(wrapped, &target) {
fmt.Println(target.Name()) // "AuthError"
}
// Generic helpers (Go 1.18+)ife, ok:= errors.AsType[*MyError](err); ok { ... }
if errors.IsType[*MyError](err) { ... }
found, ok:=errors.FindType(err, func(e*MyError) bool {
returne.Code() ==404
})
codes:=errors.Map(err, func(e*MyError) int { returne.Code() })
errors.Filter[*MyError](err) // [] *MyError from chain
errors.FirstOfType[*MyError](err) // first *MyError

Is() string-equality note(*Error).Is falls back to string comparison as a convenience for matching stdlib errors by message. For strict identity matching use Const().

Multi-error aggregation

// Basicm:=errors.NewMultiError()
m.Add(errors.New("name required"))
m.Add(errors.New("email invalid"))
fmt.Println(m.Count()) // 2// With limits and samplingm:=errors.NewMultiError(
errors.WithLimit(100),
errors.WithSampling(10), // 10% sample rate
)
// Custom formatterm:=errors.NewMultiError(
errors.WithFormatter(func(errs []error) string {
returnfmt.Sprintf("%d errors", len(errs))
}),
)
// Inspectm.First() // first errorm.Last() // last errorm.Errors() // []error snapshotm.Has() // boolm.Single() // nil | first error | *MultiError// FilternetworkErrs:=m.Filter(func(eerror) bool {
returnstrings.Contains(e.Error(), "network")
})
// Merge two MultiErrorsm.Merge(other)
// Join is a convenience that collapses errors to *MultiError or nilerr:=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(attemptint, errerror) {
log.Printf("attempt %d: %v", attempt, err)
}),
)
err:=retry.Execute(func() error {
returncallExternalService()
})
// Generic version — preserves return valueresult, err:=errors.ExecuteReply[string](retry, func() (string, error) {
returnfetchData()
})
// Context-awarectx, cancel:=context.WithTimeout(context.Background(), 5*time.Second)
defercancel()
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()
iferr:=chain.Run(); err!=nil {
errors.Inspect(err, os.Stderr)
}
// Run all steps, collect every erroriferr:=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)
returnerr
})

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 *MultiErrorerr:=errors.Drain(errs)
// First — return first non-nil error; ctx for deadline only, caller owns cancelerr:=errors.First(ctx, errs)
iferr!=nil {
cancel() // caller decides to stop siblings
}
// Collect — bounded sample; wraps ErrLimitReached when n is hiterr:=errors.Collect(ctx, errs, 10)
iferrors.Is(err, errors.ErrLimitReached) {
log.Warn("more than 10 errors — some dropped")
}
// Fan — merge multiple error channels; caller must drain or cancel to avoid leakmerged:=errors.Fan(ctx, validateErrs, enrichErrs)
forerr:=rangemerged {
log.Println(err)
}

Stream — concurrent item processing

// Process items concurrently, collect all errorss:=errors.NewStream(ctx, urls, func(urlstring) error {
returnfetch(url)
}, 8) // 8 workers; omit for len(items) workers// Option A — block until doneiferr:=s.Wait(); err!=nil {
errors.Inspect(err, os.Stderr)
}
// Option B — process errors as they arrives.Each(func(errerror) {
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 codestatus:=errors.HTTPStatusCode(err, http.StatusInternalServerError)
// Write HTTP error responseerrors.HTTPError(w, err) // plain text, status from err.Code()// With optionserrors.HTTPError(w, err,
errors.WithFallbackCode(http.StatusBadGateway),
errors.WithBody(false), // header onlyerrors.WithBodyFunc(func(eerror) string {
returnfmt.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 { returnvalidateUser(id) })
g.Go(func() error { returnvalidatePerms(id) })
iferr:=g.Wait(); err!=nil {
// err is *MultiError containing every failureerrors.Inspect(err, os.Stderr)
}
// Context-awareg:=errors.NewGroup(
errors.GroupWithContext(ctx, true), // cancelOnFirst=trueerrors.GroupWithLimit(50),
)
g.GoCtx(func(ctx context.Context) error {
returnlongRunningCheck(ctx)
})
_=g.Wait()

Inspect

// Default — writes to os.Stderrerrors.Inspect(err)
// Targeted outputvarbuf bytes.Buffererrors.Inspect(err, &buf)
// Multiple destinationserrors.Inspect(err, os.Stderr, logFile)
// Optionserrors.Inspect(err, os.Stderr,
errors.WithStackFrames(5),
errors.WithMaxDepth(20),
)
// *Error-specific convenienceerrors.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.causeslog.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 configerrors.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")
defererr.Free()
// Copy without affecting originalcopied:=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 templatevarErrDBQuery=errmgr.Define("DBQuery", "database query failed: %s")
// Instantiate with argumentserr:=ErrDBQuery("SELECT timed out")
fmt.Println(err) // "database query failed: SELECT timed out"fmt.Println(err.Category()) // "database"

Predefined errors

err:=errmgr.ErrNotFoundfmt.Println(err.Code()) // 404err:=errmgr.ErrDBQuery("SELECT failed")

Threshold monitoring

netErr:=errmgr.Define("NetError", "network issue: %s")
monitor:=errmgr.NewMonitor("NetError")
errmgr.SetThreshold("NetError", 3)
defermonitor.Close()
gofunc() {
foralert:=rangemonitor.Alerts() {
fmt.Printf("alert: %s (count: %d)\n", alert, alert.Count())
}
}()
err:=netErr("timeout")
err.Free()

Key design decisions:

  • PoolNew 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 capturecaptureStack 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

// Beforeerr:=fmt.Errorf("user %s not found: %w", username, cause)
// After — same output, plus context, code, and chain traversalerr:=errors.Newf("user %s not found: %w", username, cause).
With("username", username).
WithCode(404)

From pkg/errors

// Beforeerr:=pkgerrors.Wrap(cause, "operation failed")
// Aftererr:=errors.New("operation failed").Wrap(cause).WithStack()

Stdlib errors.Is / errors.As compatibility

// Fully compatible — no changes needediferrors.Is(err, io.EOF) { ... }
vartarget*errors.Erroriferrors.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

Contributing

Fork → branch → commit → PR. Please include tests for new behaviour and run go test -count=10 -race ./... before opening a PR.

License

MIT — see LICENSE.

About

A production-grade error handling library for Go, offering zero-cost abstractions, stack traces, multi-error support, retries, and advanced monitoring through two complementary packages: errors (core) and errmgr (management).

Resources

Stars

29 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

93 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

errors — production-grade error handling for Go

Go ReferenceGo Report CardLicenseGo 1.21+

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

go get github.com/olekukonko/errors@latest

Requires Go 1.21 or later.


Package overview

PackagePurpose
errorsCore error type, wrapping, context, stack traces, retry, chain, multi-error, channel utilities
errmgrParameterised error templates, occurrence monitoring, threshold alerting

Core — errors

Creating errors

// Fast — no stack trace, 0 allocations with poolingerr:=errors.New("connection failed")
// Formatted — full fmt verb support including %werr:=errors.Newf("user %s not found", "alice")
err:=errors.Errorf("query failed: %w", cause) // alias of Newf// With stack traceerr:=errors.Trace("critical issue")
err:=errors.Tracef("query %s failed: %w", query, cause)
// Named — useful for sentinel-style matchingerr:=errors.Named("AuthError")
// Standard library compatibleerr:=errors.Std("connection failed") // returns plain errorerr:=errors.Stdf("error %s", "detail") // formatted plain error

Stack traces

// Capture at creationerr:=errors.Trace("critical issue")
// Add to an existing errorerr=err.WithStack()
// Read framesfor_, frame:=rangeerr.Stack() {
fmt.Println(frame) // "main.go:42 main.main"
}
// Lightweight version (file:line only, no function names)for_, frame:=rangeerr.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 backctx:=errors.Context(err) // map[user_id:123 attempt:3 retryable:true]// Check for a keyiferr.HasContextKey("user_id") { ... }
// Variadic bulk attacherr.With("k1", v1, "k2", v2)
// Semantic helperserr.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)
// Traversefori, e:=rangeerrors.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 chainiferrors.Is(err, ErrNotFound) { ... }
// Add call-site context without losing the sentinelerr:=ErrNotFound.With("user 42 not found")
errors.Is(err, ErrNotFound) // true — sentinel is the cause// JSON and slog work automaticallyb, _:=json.Marshal(ErrNotFound) // {"error":"resource not found","code":"not_found"}slog.Error("lookup failed", "err", ErrNotFound)

Const vs errmgr.Defineerrors.Const — static comparable value for errors.Is matching. errmgr.Define — parameterised factory that creates new *Error instances from a format template.

Type assertions — Is / As

// Is — checks identity or name matcherr:=errors.Named("AuthError")
wrapped:=errors.Wrapf(err, "login failed")
errors.Is(wrapped, err) // true// As — extract the first matching *Error from the chainvartarget*errors.Erroriferrors.As(wrapped, &target) {
fmt.Println(target.Name()) // "AuthError"
}
// Generic helpers (Go 1.18+)ife, ok:= errors.AsType[*MyError](err); ok { ... }
if errors.IsType[*MyError](err) { ... }
found, ok:=errors.FindType(err, func(e*MyError) bool {
returne.Code() ==404
})
codes:=errors.Map(err, func(e*MyError) int { returne.Code() })
errors.Filter[*MyError](err) // [] *MyError from chain
errors.FirstOfType[*MyError](err) // first *MyError

Is() string-equality note(*Error).Is falls back to string comparison as a convenience for matching stdlib errors by message. For strict identity matching use Const().

Multi-error aggregation

// Basicm:=errors.NewMultiError()
m.Add(errors.New("name required"))
m.Add(errors.New("email invalid"))
fmt.Println(m.Count()) // 2// With limits and samplingm:=errors.NewMultiError(
errors.WithLimit(100),
errors.WithSampling(10), // 10% sample rate
)
// Custom formatterm:=errors.NewMultiError(
errors.WithFormatter(func(errs []error) string {
returnfmt.Sprintf("%d errors", len(errs))
}),
)
// Inspectm.First() // first errorm.Last() // last errorm.Errors() // []error snapshotm.Has() // boolm.Single() // nil | first error | *MultiError// FilternetworkErrs:=m.Filter(func(eerror) bool {
returnstrings.Contains(e.Error(), "network")
})
// Merge two MultiErrorsm.Merge(other)
// Join is a convenience that collapses errors to *MultiError or nilerr:=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(attemptint, errerror) {
log.Printf("attempt %d: %v", attempt, err)
}),
)
err:=retry.Execute(func() error {
returncallExternalService()
})
// Generic version — preserves return valueresult, err:=errors.ExecuteReply[string](retry, func() (string, error) {
returnfetchData()
})
// Context-awarectx, cancel:=context.WithTimeout(context.Background(), 5*time.Second)
defercancel()
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()
iferr:=chain.Run(); err!=nil {
errors.Inspect(err, os.Stderr)
}
// Run all steps, collect every erroriferr:=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)
returnerr
})

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 *MultiErrorerr:=errors.Drain(errs)
// First — return first non-nil error; ctx for deadline only, caller owns cancelerr:=errors.First(ctx, errs)
iferr!=nil {
cancel() // caller decides to stop siblings
}
// Collect — bounded sample; wraps ErrLimitReached when n is hiterr:=errors.Collect(ctx, errs, 10)
iferrors.Is(err, errors.ErrLimitReached) {
log.Warn("more than 10 errors — some dropped")
}
// Fan — merge multiple error channels; caller must drain or cancel to avoid leakmerged:=errors.Fan(ctx, validateErrs, enrichErrs)
forerr:=rangemerged {
log.Println(err)
}

Stream — concurrent item processing

// Process items concurrently, collect all errorss:=errors.NewStream(ctx, urls, func(urlstring) error {
returnfetch(url)
}, 8) // 8 workers; omit for len(items) workers// Option A — block until doneiferr:=s.Wait(); err!=nil {
errors.Inspect(err, os.Stderr)
}
// Option B — process errors as they arrives.Each(func(errerror) {
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 codestatus:=errors.HTTPStatusCode(err, http.StatusInternalServerError)
// Write HTTP error responseerrors.HTTPError(w, err) // plain text, status from err.Code()// With optionserrors.HTTPError(w, err,
errors.WithFallbackCode(http.StatusBadGateway),
errors.WithBody(false), // header onlyerrors.WithBodyFunc(func(eerror) string {
returnfmt.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 { returnvalidateUser(id) })
g.Go(func() error { returnvalidatePerms(id) })
iferr:=g.Wait(); err!=nil {
// err is *MultiError containing every failureerrors.Inspect(err, os.Stderr)
}
// Context-awareg:=errors.NewGroup(
errors.GroupWithContext(ctx, true), // cancelOnFirst=trueerrors.GroupWithLimit(50),
)
g.GoCtx(func(ctx context.Context) error {
returnlongRunningCheck(ctx)
})
_=g.Wait()

Inspect

// Default — writes to os.Stderrerrors.Inspect(err)
// Targeted outputvarbuf bytes.Buffererrors.Inspect(err, &buf)
// Multiple destinationserrors.Inspect(err, os.Stderr, logFile)
// Optionserrors.Inspect(err, os.Stderr,
errors.WithStackFrames(5),
errors.WithMaxDepth(20),
)
// *Error-specific convenienceerrors.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.causeslog.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 configerrors.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")
defererr.Free()
// Copy without affecting originalcopied:=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 templatevarErrDBQuery=errmgr.Define("DBQuery", "database query failed: %s")
// Instantiate with argumentserr:=ErrDBQuery("SELECT timed out")
fmt.Println(err) // "database query failed: SELECT timed out"fmt.Println(err.Category()) // "database"

Predefined errors

err:=errmgr.ErrNotFoundfmt.Println(err.Code()) // 404err:=errmgr.ErrDBQuery("SELECT failed")

Threshold monitoring

netErr:=errmgr.Define("NetError", "network issue: %s")
monitor:=errmgr.NewMonitor("NetError")
errmgr.SetThreshold("NetError", 3)
defermonitor.Close()
gofunc() {
foralert:=rangemonitor.Alerts() {
fmt.Printf("alert: %s (count: %d)\n", alert, alert.Count())
}
}()
err:=netErr("timeout")
err.Free()

Key design decisions:

  • PoolNew 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 capturecaptureStack 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

// Beforeerr:=fmt.Errorf("user %s not found: %w", username, cause)
// After — same output, plus context, code, and chain traversalerr:=errors.Newf("user %s not found: %w", username, cause).
With("username", username).
WithCode(404)

From pkg/errors

// Beforeerr:=pkgerrors.Wrap(cause, "operation failed")
// Aftererr:=errors.New("operation failed").Wrap(cause).WithStack()

Stdlib errors.Is / errors.As compatibility

// Fully compatible — no changes needediferrors.Is(err, io.EOF) { ... }
vartarget*errors.Erroriferrors.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

Contributing

Fork → branch → commit → PR. Please include tests for new behaviour and run go test -count=10 -race ./... before opening a PR.

License

MIT — see LICENSE.

About

A production-grade error handling library for Go, offering zero-cost abstractions, stack traces, multi-error support, retries, and advanced monitoring through two complementary packages: errors (core) and errmgr (management).

Resources

Stars

29 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

93 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

errors — production-grade error handling for Go

Go ReferenceGo Report CardLicenseGo 1.21+

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

go get github.com/olekukonko/errors@latest

Requires Go 1.21 or later.


Package overview

PackagePurpose
errorsCore error type, wrapping, context, stack traces, retry, chain, multi-error, channel utilities
errmgrParameterised error templates, occurrence monitoring, threshold alerting

Core — errors

Creating errors

// Fast — no stack trace, 0 allocations with poolingerr:=errors.New("connection failed")
// Formatted — full fmt verb support including %werr:=errors.Newf("user %s not found", "alice")
err:=errors.Errorf("query failed: %w", cause) // alias of Newf// With stack traceerr:=errors.Trace("critical issue")
err:=errors.Tracef("query %s failed: %w", query, cause)
// Named — useful for sentinel-style matchingerr:=errors.Named("AuthError")
// Standard library compatibleerr:=errors.Std("connection failed") // returns plain errorerr:=errors.Stdf("error %s", "detail") // formatted plain error

Stack traces

// Capture at creationerr:=errors.Trace("critical issue")
// Add to an existing errorerr=err.WithStack()
// Read framesfor_, frame:=rangeerr.Stack() {
fmt.Println(frame) // "main.go:42 main.main"
}
// Lightweight version (file:line only, no function names)for_, frame:=rangeerr.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 backctx:=errors.Context(err) // map[user_id:123 attempt:3 retryable:true]// Check for a keyiferr.HasContextKey("user_id") { ... }
// Variadic bulk attacherr.With("k1", v1, "k2", v2)
// Semantic helperserr.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)
// Traversefori, e:=rangeerrors.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 chainiferrors.Is(err, ErrNotFound) { ... }
// Add call-site context without losing the sentinelerr:=ErrNotFound.With("user 42 not found")
errors.Is(err, ErrNotFound) // true — sentinel is the cause// JSON and slog work automaticallyb, _:=json.Marshal(ErrNotFound) // {"error":"resource not found","code":"not_found"}slog.Error("lookup failed", "err", ErrNotFound)

Const vs errmgr.Defineerrors.Const — static comparable value for errors.Is matching. errmgr.Define — parameterised factory that creates new *Error instances from a format template.

Type assertions — Is / As

// Is — checks identity or name matcherr:=errors.Named("AuthError")
wrapped:=errors.Wrapf(err, "login failed")
errors.Is(wrapped, err) // true// As — extract the first matching *Error from the chainvartarget*errors.Erroriferrors.As(wrapped, &target) {
fmt.Println(target.Name()) // "AuthError"
}
// Generic helpers (Go 1.18+)ife, ok:= errors.AsType[*MyError](err); ok { ... }
if errors.IsType[*MyError](err) { ... }
found, ok:=errors.FindType(err, func(e*MyError) bool {
returne.Code() ==404
})
codes:=errors.Map(err, func(e*MyError) int { returne.Code() })
errors.Filter[*MyError](err) // [] *MyError from chain
errors.FirstOfType[*MyError](err) // first *MyError

Is() string-equality note(*Error).Is falls back to string comparison as a convenience for matching stdlib errors by message. For strict identity matching use Const().

Multi-error aggregation

// Basicm:=errors.NewMultiError()
m.Add(errors.New("name required"))
m.Add(errors.New("email invalid"))
fmt.Println(m.Count()) // 2// With limits and samplingm:=errors.NewMultiError(
errors.WithLimit(100),
errors.WithSampling(10), // 10% sample rate
)
// Custom formatterm:=errors.NewMultiError(
errors.WithFormatter(func(errs []error) string {
returnfmt.Sprintf("%d errors", len(errs))
}),
)
// Inspectm.First() // first errorm.Last() // last errorm.Errors() // []error snapshotm.Has() // boolm.Single() // nil | first error | *MultiError// FilternetworkErrs:=m.Filter(func(eerror) bool {
returnstrings.Contains(e.Error(), "network")
})
// Merge two MultiErrorsm.Merge(other)
// Join is a convenience that collapses errors to *MultiError or nilerr:=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(attemptint, errerror) {
log.Printf("attempt %d: %v", attempt, err)
}),
)
err:=retry.Execute(func() error {
returncallExternalService()
})
// Generic version — preserves return valueresult, err:=errors.ExecuteReply[string](retry, func() (string, error) {
returnfetchData()
})
// Context-awarectx, cancel:=context.WithTimeout(context.Background(), 5*time.Second)
defercancel()
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()
iferr:=chain.Run(); err!=nil {
errors.Inspect(err, os.Stderr)
}
// Run all steps, collect every erroriferr:=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)
returnerr
})

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 *MultiErrorerr:=errors.Drain(errs)
// First — return first non-nil error; ctx for deadline only, caller owns cancelerr:=errors.First(ctx, errs)
iferr!=nil {
cancel() // caller decides to stop siblings
}
// Collect — bounded sample; wraps ErrLimitReached when n is hiterr:=errors.Collect(ctx, errs, 10)
iferrors.Is(err, errors.ErrLimitReached) {
log.Warn("more than 10 errors — some dropped")
}
// Fan — merge multiple error channels; caller must drain or cancel to avoid leakmerged:=errors.Fan(ctx, validateErrs, enrichErrs)
forerr:=rangemerged {
log.Println(err)
}

Stream — concurrent item processing

// Process items concurrently, collect all errorss:=errors.NewStream(ctx, urls, func(urlstring) error {
returnfetch(url)
}, 8) // 8 workers; omit for len(items) workers// Option A — block until doneiferr:=s.Wait(); err!=nil {
errors.Inspect(err, os.Stderr)
}
// Option B — process errors as they arrives.Each(func(errerror) {
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 codestatus:=errors.HTTPStatusCode(err, http.StatusInternalServerError)
// Write HTTP error responseerrors.HTTPError(w, err) // plain text, status from err.Code()// With optionserrors.HTTPError(w, err,
errors.WithFallbackCode(http.StatusBadGateway),
errors.WithBody(false), // header onlyerrors.WithBodyFunc(func(eerror) string {
returnfmt.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 { returnvalidateUser(id) })
g.Go(func() error { returnvalidatePerms(id) })
iferr:=g.Wait(); err!=nil {
// err is *MultiError containing every failureerrors.Inspect(err, os.Stderr)
}
// Context-awareg:=errors.NewGroup(
errors.GroupWithContext(ctx, true), // cancelOnFirst=trueerrors.GroupWithLimit(50),
)
g.GoCtx(func(ctx context.Context) error {
returnlongRunningCheck(ctx)
})
_=g.Wait()

Inspect

// Default — writes to os.Stderrerrors.Inspect(err)
// Targeted outputvarbuf bytes.Buffererrors.Inspect(err, &buf)
// Multiple destinationserrors.Inspect(err, os.Stderr, logFile)
// Optionserrors.Inspect(err, os.Stderr,
errors.WithStackFrames(5),
errors.WithMaxDepth(20),
)
// *Error-specific convenienceerrors.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.causeslog.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 configerrors.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")
defererr.Free()
// Copy without affecting originalcopied:=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 templatevarErrDBQuery=errmgr.Define("DBQuery", "database query failed: %s")
// Instantiate with argumentserr:=ErrDBQuery("SELECT timed out")
fmt.Println(err) // "database query failed: SELECT timed out"fmt.Println(err.Category()) // "database"

Predefined errors

err:=errmgr.ErrNotFoundfmt.Println(err.Code()) // 404err:=errmgr.ErrDBQuery("SELECT failed")

Threshold monitoring

netErr:=errmgr.Define("NetError", "network issue: %s")
monitor:=errmgr.NewMonitor("NetError")
errmgr.SetThreshold("NetError", 3)
defermonitor.Close()
gofunc() {
foralert:=rangemonitor.Alerts() {
fmt.Printf("alert: %s (count: %d)\n", alert, alert.Count())
}
}()
err:=netErr("timeout")
err.Free()

Key design decisions:

  • PoolNew 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 capturecaptureStack 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

// Beforeerr:=fmt.Errorf("user %s not found: %w", username, cause)
// After — same output, plus context, code, and chain traversalerr:=errors.Newf("user %s not found: %w", username, cause).
With("username", username).
WithCode(404)

From pkg/errors

// Beforeerr:=pkgerrors.Wrap(cause, "operation failed")
// Aftererr:=errors.New("operation failed").Wrap(cause).WithStack()

Stdlib errors.Is / errors.As compatibility

// Fully compatible — no changes needediferrors.Is(err, io.EOF) { ... }
vartarget*errors.Erroriferrors.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

Contributing

Fork → branch → commit → PR. Please include tests for new behaviour and run go test -count=10 -race ./... before opening a PR.

License

MIT — see LICENSE.

About

A production-grade error handling library for Go, offering zero-cost abstractions, stack traces, multi-error support, retries, and advanced monitoring through two complementary packages: errors (core) and errmgr (management).

Resources

Stars

29 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

93 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

errors — production-grade error handling for Go

Go ReferenceGo Report CardLicenseGo 1.21+

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

go get github.com/olekukonko/errors@latest

Requires Go 1.21 or later.


Package overview

PackagePurpose
errorsCore error type, wrapping, context, stack traces, retry, chain, multi-error, channel utilities
errmgrParameterised error templates, occurrence monitoring, threshold alerting

Core — errors

Creating errors

// Fast — no stack trace, 0 allocations with poolingerr:=errors.New("connection failed")
// Formatted — full fmt verb support including %werr:=errors.Newf("user %s not found", "alice")
err:=errors.Errorf("query failed: %w", cause) // alias of Newf// With stack traceerr:=errors.Trace("critical issue")
err:=errors.Tracef("query %s failed: %w", query, cause)
// Named — useful for sentinel-style matchingerr:=errors.Named("AuthError")
// Standard library compatibleerr:=errors.Std("connection failed") // returns plain errorerr:=errors.Stdf("error %s", "detail") // formatted plain error

Stack traces

// Capture at creationerr:=errors.Trace("critical issue")
// Add to an existing errorerr=err.WithStack()
// Read framesfor_, frame:=rangeerr.Stack() {
fmt.Println(frame) // "main.go:42 main.main"
}
// Lightweight version (file:line only, no function names)for_, frame:=rangeerr.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 backctx:=errors.Context(err) // map[user_id:123 attempt:3 retryable:true]// Check for a keyiferr.HasContextKey("user_id") { ... }
// Variadic bulk attacherr.With("k1", v1, "k2", v2)
// Semantic helperserr.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)
// Traversefori, e:=rangeerrors.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 chainiferrors.Is(err, ErrNotFound) { ... }
// Add call-site context without losing the sentinelerr:=ErrNotFound.With("user 42 not found")
errors.Is(err, ErrNotFound) // true — sentinel is the cause// JSON and slog work automaticallyb, _:=json.Marshal(ErrNotFound) // {"error":"resource not found","code":"not_found"}slog.Error("lookup failed", "err", ErrNotFound)

Const vs errmgr.Defineerrors.Const — static comparable value for errors.Is matching. errmgr.Define — parameterised factory that creates new *Error instances from a format template.

Type assertions — Is / As

// Is — checks identity or name matcherr:=errors.Named("AuthError")
wrapped:=errors.Wrapf(err, "login failed")
errors.Is(wrapped, err) // true// As — extract the first matching *Error from the chainvartarget*errors.Erroriferrors.As(wrapped, &target) {
fmt.Println(target.Name()) // "AuthError"
}
// Generic helpers (Go 1.18+)ife, ok:= errors.AsType[*MyError](err); ok { ... }
if errors.IsType[*MyError](err) { ... }
found, ok:=errors.FindType(err, func(e*MyError) bool {
returne.Code() ==404
})
codes:=errors.Map(err, func(e*MyError) int { returne.Code() })
errors.Filter[*MyError](err) // [] *MyError from chain
errors.FirstOfType[*MyError](err) // first *MyError

Is() string-equality note(*Error).Is falls back to string comparison as a convenience for matching stdlib errors by message. For strict identity matching use Const().

Multi-error aggregation

// Basicm:=errors.NewMultiError()
m.Add(errors.New("name required"))
m.Add(errors.New("email invalid"))
fmt.Println(m.Count()) // 2// With limits and samplingm:=errors.NewMultiError(
errors.WithLimit(100),
errors.WithSampling(10), // 10% sample rate
)
// Custom formatterm:=errors.NewMultiError(
errors.WithFormatter(func(errs []error) string {
returnfmt.Sprintf("%d errors", len(errs))
}),
)
// Inspectm.First() // first errorm.Last() // last errorm.Errors() // []error snapshotm.Has() // boolm.Single() // nil | first error | *MultiError// FilternetworkErrs:=m.Filter(func(eerror) bool {
returnstrings.Contains(e.Error(), "network")
})
// Merge two MultiErrorsm.Merge(other)
// Join is a convenience that collapses errors to *MultiError or nilerr:=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(attemptint, errerror) {
log.Printf("attempt %d: %v", attempt, err)
}),
)
err:=retry.Execute(func() error {
returncallExternalService()
})
// Generic version — preserves return valueresult, err:=errors.ExecuteReply[string](retry, func() (string, error) {
returnfetchData()
})
// Context-awarectx, cancel:=context.WithTimeout(context.Background(), 5*time.Second)
defercancel()
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()
iferr:=chain.Run(); err!=nil {
errors.Inspect(err, os.Stderr)
}
// Run all steps, collect every erroriferr:=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)
returnerr
})

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 *MultiErrorerr:=errors.Drain(errs)
// First — return first non-nil error; ctx for deadline only, caller owns cancelerr:=errors.First(ctx, errs)
iferr!=nil {
cancel() // caller decides to stop siblings
}
// Collect — bounded sample; wraps ErrLimitReached when n is hiterr:=errors.Collect(ctx, errs, 10)
iferrors.Is(err, errors.ErrLimitReached) {
log.Warn("more than 10 errors — some dropped")
}
// Fan — merge multiple error channels; caller must drain or cancel to avoid leakmerged:=errors.Fan(ctx, validateErrs, enrichErrs)
forerr:=rangemerged {
log.Println(err)
}

Stream — concurrent item processing

// Process items concurrently, collect all errorss:=errors.NewStream(ctx, urls, func(urlstring) error {
returnfetch(url)
}, 8) // 8 workers; omit for len(items) workers// Option A — block until doneiferr:=s.Wait(); err!=nil {
errors.Inspect(err, os.Stderr)
}
// Option B — process errors as they arrives.Each(func(errerror) {
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 codestatus:=errors.HTTPStatusCode(err, http.StatusInternalServerError)
// Write HTTP error responseerrors.HTTPError(w, err) // plain text, status from err.Code()// With optionserrors.HTTPError(w, err,
errors.WithFallbackCode(http.StatusBadGateway),
errors.WithBody(false), // header onlyerrors.WithBodyFunc(func(eerror) string {
returnfmt.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 { returnvalidateUser(id) })
g.Go(func() error { returnvalidatePerms(id) })
iferr:=g.Wait(); err!=nil {
// err is *MultiError containing every failureerrors.Inspect(err, os.Stderr)
}
// Context-awareg:=errors.NewGroup(
errors.GroupWithContext(ctx, true), // cancelOnFirst=trueerrors.GroupWithLimit(50),
)
g.GoCtx(func(ctx context.Context) error {
returnlongRunningCheck(ctx)
})
_=g.Wait()

Inspect

// Default — writes to os.Stderrerrors.Inspect(err)
// Targeted outputvarbuf bytes.Buffererrors.Inspect(err, &buf)
// Multiple destinationserrors.Inspect(err, os.Stderr, logFile)
// Optionserrors.Inspect(err, os.Stderr,
errors.WithStackFrames(5),
errors.WithMaxDepth(20),
)
// *Error-specific convenienceerrors.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.causeslog.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 configerrors.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")
defererr.Free()
// Copy without affecting originalcopied:=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 templatevarErrDBQuery=errmgr.Define("DBQuery", "database query failed: %s")
// Instantiate with argumentserr:=ErrDBQuery("SELECT timed out")
fmt.Println(err) // "database query failed: SELECT timed out"fmt.Println(err.Category()) // "database"

Predefined errors

err:=errmgr.ErrNotFoundfmt.Println(err.Code()) // 404err:=errmgr.ErrDBQuery("SELECT failed")

Threshold monitoring

netErr:=errmgr.Define("NetError", "network issue: %s")
monitor:=errmgr.NewMonitor("NetError")
errmgr.SetThreshold("NetError", 3)
defermonitor.Close()
gofunc() {
foralert:=rangemonitor.Alerts() {
fmt.Printf("alert: %s (count: %d)\n", alert, alert.Count())
}
}()
err:=netErr("timeout")
err.Free()

Key design decisions:

  • PoolNew 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 capturecaptureStack 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

// Beforeerr:=fmt.Errorf("user %s not found: %w", username, cause)
// After — same output, plus context, code, and chain traversalerr:=errors.Newf("user %s not found: %w", username, cause).
With("username", username).
WithCode(404)

From pkg/errors

// Beforeerr:=pkgerrors.Wrap(cause, "operation failed")
// Aftererr:=errors.New("operation failed").Wrap(cause).WithStack()

Stdlib errors.Is / errors.As compatibility

// Fully compatible — no changes needediferrors.Is(err, io.EOF) { ... }
vartarget*errors.Erroriferrors.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

Contributing

Fork → branch → commit → PR. Please include tests for new behaviour and run go test -count=10 -race ./... before opening a PR.

License

MIT — see LICENSE.

About

A production-grade error handling library for Go, offering zero-cost abstractions, stack traces, multi-error support, retries, and advanced monitoring through two complementary packages: errors (core) and errmgr (management).

Resources

Stars

29 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages