Wrap any error with any data stucture using generics; automatically log that data, or extract directly it later. Loggable stacktraces out of the box.
v1.x releases make no breaking changes to exported APIs. New functionality may be added in minor releases; patches are bug fixes, or administrative work only.
Go 1.26.1 or later.
go get github.com/wood-jp/xerrorsExtend wraps an error with a value of any type. Passing nil returns nil.
type RequestContext struct {
UserID string
RequestID string
}
err := xerrors.Extend(RequestContext{UserID: "123", RequestID: "abc"}, originalErr)Extract walks the error chain and returns the first value of the requested type. If the same type has been extended more than once, you get the outermost one.
if rctx, ok := xerrors.Extract[RequestContext](err); ok {
fmt.Println(rctx.UserID)
}This works through multiple layers of wrapping:
err := xerrors.Extend(myData, originalErr)
wrapped := fmt.Errorf("operation failed: %w", err)
data, ok := xerrors.Extract[MyData](wrapped) // still worksExtendedError implements slog.LogValuer, so logging a wrapped error works out of the box by walking the full chain and collecting everything into one flat structure:
{
"error": "something went wrong",
"error_detail": {
"class": "transient",
"stacktrace": [...],
"context": { "user_id": "123" }
}
}xerrors.Log(err) returns that as a ready-to-use slog.Attr with the key "error". Just drop it into any slog call:
logger.Error("request failed", xerrors.Log(err))Data types contribute to error_detail by implementing slog.LogValuer and returning a group value. The attrs in that group are merged directly into error_detail. Types that don't implement slog.LogValuer, or whose LogValue doesn't resolve to a group, fall back to a single "data" key. See sub-packages for examples.
Extend(nil)returns nil- If you extend the same type more than once,
Extractreturns the outermost one - Type aliases are distinct:
type A intandtype B intdon't match each other
WARNING: This should not be used in conjuction with
errors.Joinas the resulting joined error may have unexpected behavior.
github.com/wood-jp/xerrors/errclass
Attaches a severity class to an error so callers can decide whether to retry.
Classes are ordered by severity:
| Class | Description |
|---|---|
Nil |
No error (nil) |
Unknown |
Unclassified (zero value) |
Transient |
May succeed on retry |
Persistent |
Will not resolve on retry |
Panic |
Came from a recovered panic |
By default, WrapAs always applies the class unconditionally:
// Wraps regardless of whether err already has a class
err := errclass.WrapAs(err, errclass.Transient)
class := errclass.GetClass(err)
if class == errclass.Transient {
// retry
}Two options let you restrict when wrapping happens:
// Only classify if the error has no class yet — leaves already-classified errors alone
err = errclass.WrapAs(err, errclass.Persistent, errclass.WithOnlyUnknown())
// Only classify if the new class is more severe than the current one — useful for escalation
err = errclass.WrapAs(err, errclass.Panic, errclass.WithOnlyMoreSevere())Class implements slog.LogValuer. It shows up as "class": "transient" in flat log output.
errors.Join is not supported. Class information on individual errors may be lost when combining into a joined error.
github.com/wood-jp/xerrors/errcontext
Attaches slog.Attr key-value pairs to an error. Useful for carrying request-scoped fields through a call stack without threading them through every function signature.
// Attach context
err := errcontext.Add(err, slog.String("user_id", "123"), slog.Int("attempt", 3))
// Add more later — the existing map is updated in place, no extra wrapper
err = errcontext.Add(err, slog.String("request_id", "abc"))
// Pull it out
ctx := errcontext.Get(err)
if ctx != nil {
attrs := ctx.Flatten() // sorted by key for deterministic output
slog.Info("request failed", attrs...)
}Context implements slog.LogValuer. Attached keys appear under "context" in flat log output.
Add with nil returns nil. Add with no attrs is a no-op. Duplicate keys use last-write-wins. errors.Join is not supported.
github.com/wood-jp/xerrors/stacktrace
Captures a stack trace where Wrap is called and attaches it to the error. If the error already has a trace, Wrap is a no-op.
StackTrace implements slog.LogValuer, and appears as a "stacktrace" array in flat log output. For example:
var errTest = errors.New("something went wrong")
func c() error {
return stacktrace.Wrap(errclass.WrapAs(errTest, errclass.Transient))
}
func b() error { return c() }
func a() error { return b() }
err := a()
logger.Error("request failed", xerrors.Log(err))Outputs a log similar to:
{
"level": "ERROR",
"msg": "request failed",
"error": {
"error": "something went wrong",
"error_detail": {
"class": "transient",
"stacktrace": [
{"func": "main.c", "line": 16, "source": "main.go"},
{"func": "main.b", "line": 20, "source": "main.go"},
{"func": "main.a", "line": 24, "source": "main.go"},
{"func": "main.main", "line": 31, "source": "main.go"}
]
}
}
}However, if you wish to directly get at the stack trace data, you can pull the trace back out with Extract:
if st := stacktrace.Extract(err); st != nil {
// st is a []Frame with File, LineNumber, Function
}Alternatively, if you don't want to capture any stack traces but want to keep the code around, just disable them globally:
stacktrace.Disabled.Store(true)This results in all Wrap calls becoming no-ops.
github.com/wood-jp/xerrors/calm
Wraps a function call so that any panic is recovered and returned as an error with a
stack trace and an errclass.Panic classification.
err := calm.Unpanic(func() error {
// code that might panic
return doSomething()
})
if errclass.GetClass(err) == errclass.Panic {
// handle recovered panic
}Unpanic returns nil if f returns nil. If f panics, the panic value is wrapped with
fmt.Errorf("panic: %v", r), given a stack trace starting at the panic site, and classified as errclass.Panic.
If the panic was called with an error argument, the panic value is wrapped with fmt.Errorf("panic: %v", r), preserving the original error.
WARNING: It is not possible to recover from a panic in a goroutine spawned by
f(). Goroutines created insidefmust guard themselves against panics.
github.com/wood-jp/xerrors/errgroup
Wraps golang.org/x/sync/errgroup so
that panics inside goroutines are recovered and returned as errors rather than crashing the
program. Uses calm.Unpanic internally, so recovered panics carry a stack trace
and an errclass.Panic classification.
g := errgroup.New()
g.Go(func() error {
return doSomething() // panics are caught and returned as errors
})
if err := g.Wait(); err != nil {
if errclass.GetClass(err) == errclass.Panic {
// handle recovered panic
}
}WithContext works the same as upstream: the derived context is cancelled the first time a
goroutine returns a non-nil error (including a recovered panic), or when Wait returns.
SetLimit and TryGo are also available and behave identically to the upstream package,
with the same panic-recovery guarantee.
WARNING: Panics in goroutines spawned inside
f()are not recovered. Goroutines created withinfmust guard themselves — usecalm.Unpanicor callg.Goagain from withinf.
Benchmarks cover the three operations users care about: stack capture, generic wrapping/extraction, and context attachment. Run them yourself with:
just benchResults on an Intel Core Ultra 7 155H (Go 1.26.1, linux/amd64, -count=3):
goos: linux
goarch: amd64
cpu: Intel(R) Core(TM) Ultra 7 155H
pkg: github.com/wood-jp/xerrors/stacktrace
BenchmarkWrap_New-22 804999 1314 ns/op 880 B/op 5 allocs/op
BenchmarkWrap_Existing-22 64795417 19 ns/op 0 B/op 0 allocs/op
BenchmarkWrap_New_Deep-22 497211 2772 ns/op 1104 B/op 5 allocs/op
BenchmarkWrap_Existing_Deep-22 33198662 31 ns/op 0 B/op 0 allocs/op
pkg: github.com/wood-jp/xerrors
BenchmarkExtend-22 34270780 32 ns/op 48 B/op 1 allocs/op
BenchmarkExtract_Shallow-22 89609863 11 ns/op 0 B/op 0 allocs/op
BenchmarkExtract_Deep-22 28955666 42 ns/op 0 B/op 0 allocs/op
BenchmarkLog-22 1398642 857 ns/op 960 B/op 20 allocs/op
pkg: github.com/wood-jp/xerrors/errcontext
BenchmarkAdd_New-22 5845732 207 ns/op 440 B/op 4 allocs/op
BenchmarkAdd_Existing-22 48651021 22 ns/op 0 B/op 0 allocs/op
BenchmarkAdd_Existing_Deep-22 35166369 36 ns/op 0 B/op 0 allocs/op
BenchmarkFlatten-22 2332621 511 ns/op 512 B/op 8 allocs/op
As one might expect, call-depth (for stacktraces) and error-chain depth impact the actual costs. The "deep" benchmarks here only have depth/length of 5 for illustrative purposes.
Actually obtaining a stack trace is expensive, but only happens once in the call-chain. Re-wrapping an already-traced error is a no-op (aside walking the error chain).
Adding error context is also very cheap after the first. It also has an error-chain depth traversal cost if adding context at different call sites.
See CONTRIBUTING.md.
See SECURITY.md.
This library is a simplified fork of one written by wood-jp at Zircuit. The original code is available here: zkr-go-common-public/xerrors