Skip to content

traiproject/zerr

Repository files navigation

zerr - High-Performance Error Handling Library

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.

Features

  • Lazy & Deduplicated Stack Traces: Capture stack traces lazily using runtime.Callers and 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 slog Integration: Automatic structured logging with slog.LogValuer implementation
  • 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

Requirements

  • Go 1.25 or later

Installation

go get go.trai.ch/zerr

Usage

API Note: The New and Wrap functions return error to prevent the typed nil issue. You can use the global helper functions zerr.With and zerr.Stack to add context to any error without manual type assertion.

Basic Error Creation

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")

Adding Metadata

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)
}

Accessing Metadata

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.

Stack Traces

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)

Logging with slog

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)

Goroutine Safety

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")
}

Performance

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.

License

MIT

About

Production-ready, high-performance Go error handling library

Resources

Stars

Watchers

Forks