Skip to content

domonda/go-errs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

80 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-errs

Go 1.13+ compatible error wrapping with call stacks and function parameters.

Go Reference Go Report Card

Features

  • Automatic call stack capture - Every error wrapped with this package includes the full call stack at the point where the error was created or wrapped
  • Function parameter tracking - Capture and display function parameters in error messages for detailed debugging
  • Go 1.13+ error wrapping compatible - Works seamlessly with errors.Is, errors.As, and errors.Unwrap
  • Zero allocation optimization - Specialized functions for 0-10 parameters to avoid varargs allocations
  • Helper utilities - Common patterns for NotFound errors, context errors, and panic recovery
  • Customizable formatting - Control how sensitive data appears in error messages
  • Go 1.23+ iterator support - Convert errors to iterators for functional programming patterns

Installation

go get github.com/domonda/go-errs

Quick Start

Basic Error Creation

import "github.com/domonda/go-errs"

func DoSomething() error {
    return errs.New("something went wrong")
}

// Error output includes call stack:
// something went wrong
// main.DoSomething
//     /path/to/file.go:123

Wrapping Errors with Function Parameters

The most powerful feature - automatically capture function parameters when errors occur:

func ProcessUser(userID string, age int) (err error) {
    defer errs.WrapWithFuncParams(&err, userID, age)

    if age < 0 {
        return errors.New("invalid age")
    }

    return database.UpdateUser(userID, age)
}

// When an error occurs, output includes:
// invalid age
// main.ProcessUser("user-123", -5)
//     /path/to/file.go:45

Error Wrapping

func LoadConfig(path string) error {
    data, err := os.ReadFile(path)
    if err != nil {
        return errs.Errorf("failed to read config: %w", err)
    }
    // ... parse data
    return nil
}

Core Functions

Error Creation

  • errs.New(text) - Create a new error with call stack
  • errs.Errorf(format, ...args) - Format an error with call stack (supports %w for wrapping)
  • errs.Sentinel(text) - Create a const-able sentinel error

Error Wrapping with Parameters

  • errs.WrapWithFuncParams(&err, params...) - Most common: wrap error with function parameters
  • errs.WrapWith0FuncParams(&err) through errs.WrapWith10FuncParams(&err, p0, ...) - Optimized variants for specific parameter counts
  • errs.WrapWithFuncParamsSkip(skip, &err, params...) - Advanced: control stack frame skipping

Error Wrapping without Parameters

  • errs.WrapWithCallStack(err) - Wrap error with call stack only
  • errs.WrapWithCallStackSkip(skip, err) - Advanced: control stack frame skipping

Advanced Features

NotFound Errors

Standardized "not found" error handling compatible with sql.ErrNoRows and os.ErrNotExist:

var ErrUserNotFound = fmt.Errorf("user %w", errs.ErrNotFound)

func GetUser(id string) (*User, error) {
    user, err := db.Query("SELECT * FROM users WHERE id = ?", id)
    if errs.IsErrNotFound(err) {
        return nil, ErrUserNotFound
    }
    return user, err
}

// Check for any "not found" variant
if errs.IsErrNotFound(err) {
    // Handle not found case
}

Context Error Helpers

// Check if context is done
if errs.IsContextDone(ctx) {
    // Handle context done
}

// Check specific context errors
if errs.IsContextCanceled(ctx) {
    // Handle cancellation
}

if errs.IsContextDeadlineExceeded(ctx) {
    // Handle timeout
}

// Check if an error is context-related
if errs.IsContextError(err) {
    // Don't retry context errors
}

Panic Recovery

func RiskyOperation() (err error) {
    defer errs.RecoverPanicAsError(&err)

    // If this panics, it will be converted to an error
    return doSomethingRisky()
}

// With function parameters
func ProcessItem(id string) (err error) {
    defer errs.RecoverPanicAsErrorWithFuncParams(&err, id)

    return processItem(id) // May panic
}

Logging Control

type CustomError struct {
    error
}

func (e CustomError) ShouldLog() bool {
    return false // Don't log this error
}

// Check if error should be logged
if errs.ShouldLog(err) {
    logger.Error(err)
}

// Wrap error to prevent logging
err = errs.DontLog(err)

Protecting Sensitive Data

Using KeepSecret for Quick Protection

For simple cases where you want to prevent a parameter from appearing in logs, use errs.KeepSecret(param):

func Login(username string, password string) (err error) {
    defer errs.WrapWithFuncParams(&err, username, errs.KeepSecret(password))
    // Error messages will show: Login("admin", ***REDACTED***)
    return authenticate(username, password)
}

The Secret interface wraps a value and ensures it's never logged or printed:

  • String() returns "***REDACTED***"
  • Implements CallStackPrintable to ensure redaction in error call stacks
  • Use secret.Secrect() to retrieve the actual value when needed

Important: Wrapping a parameter with errs.KeepSecret() is preferable to omitting it entirely from a defer errs.WrapWith* statement. When you run go-errs-wrap replace, omitted parameters will be added back, but KeepSecret-wrapped parameters are preserved in their wrapped form.

Custom Types with CallStackPrintable

For custom types, implement CallStackPrintable to control how they appear in error messages:

type Password struct {
    value string
}

func (p Password) PrintForCallStack(w io.Writer) {
    w.Write([]byte("***REDACTED***"))
}

func Login(username string, pwd Password) (err error) {
    defer errs.WrapWithFuncParams(&err, username, pwd)
    // Error messages will show: Login("admin", ***REDACTED***)
    return authenticate(username, pwd)
}

Unwrapping and Inspection

Finding Errors by Type

// Check if error chain contains a specific type
if errs.Has[*DatabaseError](err) {
    // Handle database error
}

// Get all errors of a specific type from the chain
dbErrors := errs.As[*DatabaseError](err)
for _, dbErr := range dbErrors {
    // Handle each database error
}

// Check error type without custom Is/As methods
if errs.Type[*DatabaseError](err) {
    // Error is or wraps a DatabaseError
}

Navigating Error Chains

// Get the root cause error
rootErr := errs.Root(err)

// Unwrap call stack information only
plainErr := errs.UnwrapCallStack(err)

Go 1.23+ Iterator Support

// Convert error to single-value iterator
for err := range errs.IterSeq(myErr) {
    // Process error
}

// Convert error to two-value iterator (value, error) pattern
for val, err := range errs.IterSeq2[MyType](myErr) {
    if err != nil {
        // Handle error
    }
}

Configuration

Customize Call Stack Display

// Change path prefix trimming
errs.TrimFilePathPrefix = "/go/src/"

// Adjust maximum stack depth
errs.MaxCallStackFrames = 64 // Default is 32

Customize Function Call Formatting

// Replace the global formatter
errs.FormatFunctionCall = func(function string, params ...any) string {
    // Your custom formatting logic
    return fmt.Sprintf("%s(%v)", function, params)
}

Best Practices

1. Always use defer for WrapWithFuncParams

func MyFunc(id string) (err error) {
    defer errs.WrapWithFuncParams(&err, id)
    // Function body
}

2. Use optimized variants for better performance

// Instead of:
defer errs.WrapWithFuncParams(&err, p0, p1, p2)

// Use:
defer errs.WrapWith3FuncParams(&err, p0, p1, p2)

3. Protect sensitive data

type APIKey string

func (k APIKey) PrintForCallStack(w io.Writer) {
    io.WriteString(w, "***")
}

4. Use errs.Errorf for wrapping

// Good - preserves error chain
return errs.Errorf("failed to process user %s: %w", userID, err)

// Avoid - loses error chain
return errs.New(fmt.Sprintf("failed: %s", err))

5. Prefer errs package for all error creation

// Use errs.New instead of errors.New
return errs.New("something failed")

// Use errs.Errorf instead of fmt.Errorf
return errs.Errorf("failed: %w", err)

go-errs-wrap CLI Tool

The go-errs-wrap command-line tool helps manage defer errs.WrapWithFuncParams statements in your Go code.

Installation

go install github.com/domonda/go-errs/cmd/go-errs-wrap@latest

Commands

Command Description
remove Remove all defer errs.Wrap* or //#wrap-result-err lines
replace Replace existing defer errs.Wrap* or //#wrap-result-err with properly generated code
insert Insert defer errs.Wrap* at the first line of functions with named error results that don't already have one

Usage Examples

Insert wrap statements into all functions missing them:

# Process a single file
go-errs-wrap insert ./pkg/mypackage/file.go

# Process all Go files in a directory recursively
go-errs-wrap insert ./pkg/...

Replace outdated wrap statements with correct ones:

# Update parameters in existing wrap statements
go-errs-wrap replace ./pkg/mypackage/file.go

Remove all wrap statements:

go-errs-wrap remove ./pkg/...

Write changes to another output location:

# Output to a different location
go-errs-wrap insert -out ./output ./pkg/mypackage

# Show verbose progress
go-errs-wrap insert -verbose ./pkg/...

Options

Option Description
-out <path> Output to different location instead of modifying source
-minvariadic Use specialized WrapWithNFuncParams functions instead of variadic
-verbose Print progress information
-help Show help message

Example Transformation

Given this input file:

package example

func ProcessData(ctx context.Context, id string) (err error) {
    return doWork(ctx, id)
}

Running go-errs-wrap insert example.go produces:

package example

import "github.com/domonda/go-errs"

func ProcessData(ctx context.Context, id string) (err error) {
    defer errs.WrapWith2FuncParams(&err, ctx, id)

    return doWork(ctx, id)
}

The tool:

  • Inserts the defer errs.Wrap* statement at the first line of the function body
  • Adds an empty line after the defer statement
  • Automatically adds the required import
  • Uses the optimized function variant based on parameter count
  • Skips functions without named error results
  • Skips functions that already have a defer errs.Wrap* statement
  • Preserves errs.KeepSecret(param) wrapped parameters during replacement

Preserving Secrets During Replacement

When using go-errs-wrap replace, parameters wrapped with errs.KeepSecret() are preserved. This allows developers to mark sensitive parameters once and have that protection maintained across replacements:

// Before: developer manually wrapped password with KeepSecret
func Login(username, password string) (err error) {
    defer errs.WrapWithFuncParams(&err, username, errs.KeepSecret(password))
    return authenticate(username, password)
}

// After running: go-errs-wrap replace -minvariadic file.go
// The KeepSecret wrapping is preserved:
func Login(username, password string) (err error) {
    defer errs.WrapWith2FuncParams(&err, username, errs.KeepSecret(password))
    return authenticate(username, password)
}

This is preferable to omitting sensitive parameters entirely, as omitted parameters would be re-added by the tool.

Compatibility

  • Go version: Requires Go 1.13+ for error wrapping, Go 1.23+ for iterator support
  • Error handling: Fully compatible with errors.Is, errors.As, errors.Unwrap, and errors.Join
  • Testing: Use with testify or any testing framework

Performance

  • Zero-allocation error wrapping for functions with 0-10 parameters (using specialized functions)
  • Efficient call stack capture using runtime.Callers
  • Lazy error message formatting - only formats when Error() is called
  • Configurable stack depth to balance detail vs memory usage

Examples

See the examples directory and godoc for more examples.

License

MIT License - see LICENSE file for details.

Contributing

Contributions welcome! Please open an issue or submit a pull request.

Related Packages

  • go-pretty - Pretty printing used for error parameter formatting

About

Go 1.13 compatible error wrapping with call stacks and function parameters

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages