zerr is a production-ready, high-performance Go error handling library that provides modern, idiomatic error wrapping with lazy stack traces, deduplication, and structured metadata.
- Lazy & Deduplicated Stack Traces: Capture stack traces lazily using
runtime.Callersand deduplicate them using a global cache - Efficient & Deduplicated Stack Traces: Capture stack traces efficiently using pooled buffers and deduplicate them using a global cache, deferring expensive symbol resolution until formatting
- Low-Overhead Wrapping: Optimized happy path with minimal allocation overhead for error wrapping
- Structured Metadata: Attach typed key-value pairs to errors efficiently
- Native
slogIntegration: Automatic structured logging withslog.LogValuerimplementation - Goroutine Safety: Safe recovery from panics in goroutines with
Defer() - Typed Nil Issue Fix: Fixed the common Go bug where nil pointers with types aren't truly nil
- Go 1.25 or later
go get go.trai.ch/zerrAPI Note: The
NewandWrapfunctions returnerrorto prevent the typed nil issue. You can use the global helper functionszerr.Withandzerr.Stackto add context to any error without manual type assertion.
import "go.trai.ch/zerr"
// Create a new error
err := zerr.New("something went wrong")
// Wrap an existing error
err = zerr.Wrap(err, "failed to process request")You can add metadata using the global helper (works with any error) or by method chaining (requires casting).
// Option 1: Use the global helper (easiest)
// Automatically upgrades standard errors to zerr.Error
err := zerr.New("database error")
err = zerr.With(err, "table", "users")
// Option 2: Method chaining (fastest for multiple fields)
// Requires type assertion since New() returns standard error
if zerrErr, ok := err.(*zerr.Error); ok {
err = zerrErr.With("operation", "insert").
With("user_id", 12345)
}You can access the structured metadata attached to errors using the Metadata() method:
// Create an error with metadata
err := zerr.New("database error")
err = zerr.With(err, "table", "users")
err = zerr.With(err, "operation", "insert")
err = zerr.With(err, "user_id", 12345)
// Access metadata (requires type assertion to *zerr.Error)
if zerrErr, ok := err.(*zerr.Error); ok {
metadata := zerrErr.Metadata()
// metadata is map[string]any{"table": "users", "operation": "insert", "user_id": 12345}
// Access individual values
if table, exists := metadata["table"]; exists {
fmt.Printf("Table: %v\n", table)
}
}The Metadata() method returns a copy of the error's metadata as a map[string]any. It returns an empty map if there is no metadata attached to the error. The returned map is a copy, ensuring that modifications to it do not affect the internal state of the error.
Capture stack traces easily using the global Stack helper.
// Capture stack trace lazily
err := zerr.New("critical failure")
err = zerr.Stack(err)
// Works with standard errors too (upgrades them)
stdErr := errors.New("standard Go error")
err = zerr.Stack(stdErr)import "log/slog"
// Log errors with structured fields
zerr.Log(context.Background(), slog.Default(), err)
// Errors automatically format themselves when logged
logger.Error("operation failed", "error", err)func backgroundTask() {
defer zerr.Defer(func(err error) {
// Handle recovered errors
zerr.Log(context.Background(), slog.Default(), err)
})
// Potentially panicking code
panic("something went wrong")
}Benchmarks run on Apple M4 Pro (Go 1.25) demonstrate the efficiency of the deduplication engine.
The single allocation in New and Wrap ensures type safety (preventing typed
nil bugs), while the stack trace machinery remains zero-allocation for cached
traces.
BenchmarkNew-14 93238076 12.55 ns/op 64 B/op 1 allocs/op
BenchmarkWrap-14 92826571 12.76 ns/op 64 B/op 1 allocs/op
BenchmarkWrapWithStack-14 7189615 169.5 ns/op 128 B/op 2 allocs/op
BenchmarkWithMetadata-14 15186404 78.50 ns/op 200 B/op 4 allocs/op
BenchmarkErrorFormatting-14 752197629 1.591 ns/op 0 B/op 0 allocs/op
Note: BenchmarkWrapWithStack incurring only 1 allocation (for the error struct
itself) demonstrates the effectiveness of the global stack cache. Once a
specific stack trace is captured, adding it to an error incurs no additional
memory allocation overhead beyond the error wrapper.
The happy path (New/Wrap) is highly optimized. Heavy operations like stack tracing use internal pooling and deduplication to eliminate GC pressure in hot paths.
MIT