Go error wrapping with call-stack and function parameter capture.
- 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
- Error wrapping compatible - Works seamlessly with
errors.Is,errors.As, anderrors.Unwrap - Helper utilities - Common patterns for NotFound errors, context errors, and panic recovery
- Customizable formatting - Control how sensitive data appears in error messages
- Iterator support - Convert errors to
iter.Seqanditer.Seq2iterators
go get github.com/domonda/go-errsimport "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:123The 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:45func 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
}errs.New(text)- Create a new error with call stackerrs.Errorf(format, ...args)- Format an error with call stack (supports%wfor wrapping)errs.Sentinel(text)- Create a const-able sentinel error
errs.WrapWithFuncParams(&err, params...)- Most common: wrap error with function parameterserrs.WrapWith0FuncParams(&err)througherrs.WrapWith10FuncParams(&err, p0, ...)- Optimized variants for specific parameter countserrs.WrapWithFuncParamsSkip(skip, &err, params...)- Advanced: control stack frame skipping
errs.WrapWithCallStack(err)- Wrap error with call stack onlyerrs.WrapWithCallStackSkip(skip, err)- Advanced: control stack frame skipping
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
}// 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
}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
}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)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
pretty.Stringer(viaPrettyString()) to ensure redaction in error call stacks and pretty-printed output - Use
secret.Secret()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.
For custom types, implement one of the go-pretty interfaces to control how they appear in error messages. The interfaces are checked in priority order:
pretty.PrintableWithResult—PrettyPrint(io.Writer) (n int, err error)— full control with byte countpretty.Printable—PrettyPrint(io.Writer)— simple writer-based formattingpretty.Stringer—PrettyString() string— simplest, just return a string
// Simplest approach: implement pretty.Stringer
type Password string
func (Password) PrettyString() string {
return "***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)
}The go-pretty library automatically handles recursive formatting, so types implementing any of these interfaces
will be properly formatted even when nested in other structs.
Default behavior: By default, the Printer checks if a type implements pretty.PrintableWithResult, pretty.Printable, or pretty.Stringer (in that order). This is the recommended approach for types you own and control.
For advanced use cases where you need to customize formatting beyond implementing these interfaces, use Printer.WithPrintFuncFor(). This is useful when:
- You want to format types you don't control (third-party types, stdlib types)
- You need runtime-conditional formatting based on values or context
- You want to adapt types that implement other interfaces (e.g.,
fmt.Stringer) - You need to apply global formatting rules based on struct tags or type patterns
- You want to override or wrap the default behavior
import (
"fmt"
"io"
"reflect"
"strings"
"github.com/domonda/go-errs"
"github.com/domonda/go-pretty"
)
func init() {
// Create a custom printer with PrintFuncFor hook
errs.Printer = errs.Printer.WithPrintFuncFor(func(v reflect.Value) pretty.PrintFunc {
// Example 1: Mask strings containing sensitive keywords
if v.Kind() == reflect.String {
str := v.String()
if strings.Contains(strings.ToLower(str), "password") ||
strings.Contains(strings.ToLower(str), "token") ||
strings.Contains(strings.ToLower(str), "apikey") {
return func(w io.Writer) (int, error) {
return io.WriteString(w, "`***REDACTED***`")
}
}
}
// Example 2: Hide struct fields tagged with `secret:"true"`
if v.Kind() == reflect.Struct {
t := v.Type()
for i := range t.NumField() {
field := t.Field(i)
if field.Tag.Get("secret") == "true" {
// Create a custom formatter that masks tagged fields
return func(w io.Writer) (int, error) {
// Custom struct formatting logic here
n1, _ := io.WriteString(w, t.Name())
n2, err := io.WriteString(w, "{***FIELDS_REDACTED***}")
return n1 + n2, err
}
}
}
}
// Example 3: Adapt types implementing fmt.Stringer
stringer, ok := v.Interface().(fmt.Stringer)
if !ok && v.CanAddr() {
stringer, ok = v.Addr().Interface().(fmt.Stringer)
}
if ok {
return func(w io.Writer) (int, error) {
return io.WriteString(w, stringer.String())
}
}
// Fall back to default Printable interface handling
return pretty.PrintFuncForPrintable(v)
})
}
// Now all error call stacks will use your custom formatting
func ProcessPayment(amount int, cardNumber string) (err error) {
defer errs.WrapWithFuncParams(&err, amount, cardNumber)
// If cardNumber contains "4111-1111-1111-1111", it will be shown as:
// ProcessPayment(1000, `***REDACTED***`)
return chargeCard(amount, cardNumber)
}Key points:
- Default behavior: If no
PrintFuncForis set, thePrinterautomatically checks if types implementpretty.PrintableWithResult,pretty.Printable, orpretty.Stringer WithPrintFuncFor()returns a newPrinterwith the custom hook installed- The hook receives a
reflect.Valueand returns apretty.PrintFunc(ornilto use default) - Important: In a PrintFuncFor function, always return
pretty.PrintFuncForPrintable(v)as the fallback to preserve the default interface checking behavior. - The hook applies to all values printed in error call stacks, including nested struct fields
- This approach allows centralized control over sensitive data redaction without modifying type definitions
PrintFuncForis evaluated for every value, allowing you to intercept and customize formatting before the default interfaces are checked
Comparison: Interfaces vs PrintFuncFor
// Approach 1: Implement pretty.Stringer (recommended for types you own)
type APIKey string
func (APIKey) PrettyString() string {
return "***REDACTED***"
}
// Approach 2: Use PrintFuncFor (for types you don't control or global rules)
func init() {
errs.Printer = errs.Printer.WithPrintFuncFor(func(v reflect.Value) pretty.PrintFunc {
// Check for APIKey type from a third-party package
if v.Type().String() == "thirdparty.APIKey" {
return func(w io.Writer) (int, error) {
return io.WriteString(w, "***REDACTED***")
}
}
// Fall back to checking for default interfaces
return pretty.PrintFuncForPrintable(v)
})
}Example with struct tags:
type User struct {
ID string
Email string
Password string `secret:"true"`
APIKey string `secret:"true"`
}
// With the PrintFuncFor hook configured above,
// error messages will automatically redact fields tagged with secret:"true"Since Go 1.26, the standard library provides errors.AsType[E](err) (E, bool) which returns the first matching error and a boolean. Use it when you need the matched value:
// Go 1.26+: get first matching error (preferred when you need the value)
if dbErr, ok := errors.AsType[*DatabaseError](err); ok {
log.Println("database error:", dbErr.Code)
}This package provides additional generic helpers:
// Check if error chain contains a specific type (bool only, no value needed)
// Equivalent to: _, ok := errors.AsType[*DatabaseError](err)
if errs.Has[*DatabaseError](err) {
// Handle database error
}
// Get ALL errors of a specific type from the full wrapping tree.
// Unlike errors.AsType which returns only the first match,
// errs.As traverses the entire tree including multi-errors (errors.Join):
//
// err := errors.Join(
// &ValidationError{Field: "name"},
// &ValidationError{Field: "email"},
// )
// errors.AsType[*ValidationError](err) // returns only "name"
// errs.As[*ValidationError](err) // returns both "name" and "email"
//
allErrors := errs.As[*ValidationError](err)
for _, vErr := range allErrors {
log.Println("invalid field:", vErr.Field)
}
// Check error type without custom Is/As methods
if errs.Type[*DatabaseError](err) {
// Error is or wraps a DatabaseError
}// Get the root cause error
rootErr := errs.Root(err)
// Unwrap call stack information only
plainErr := errs.UnwrapCallStack(err)// 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
}
}// Change path prefix trimming
errs.TrimFilePathPrefix = "/go/src/"
// Adjust maximum stack depth
errs.MaxCallStackFrames = 64 // Default is 32Control how long parameter values can be in error messages to prevent huge values from making errors unreadable:
// Default is 5000 bytes
errs.FormatParamMaxLen = 500
// Now long parameter values will be truncated
func ProcessDocument(content string) (err error) {
defer errs.WrapWithFuncParams(&err, content)
// If content is 2KB, error will show:
// ProcessDocument("first 500 bytes…(TRUNCATED)")
return parseDocument(content)
}This is particularly useful when dealing with:
- Large JSON or XML payloads
- Binary data encoded as strings
- Long database query results
- Large data structures
// Replace the global formatter
errs.FormatFunctionCall = func(function string, params ...any) string {
// Your custom formatting logic
return fmt.Sprintf("%s(%v)", function, params)
}func MyFunc(id string) (err error) {
defer errs.WrapWithFuncParams(&err, id)
// Function body
}// Instead of:
defer errs.WrapWithFuncParams(&err, p0, p1, p2)
// Use:
defer errs.WrapWith3FuncParams(&err, p0, p1, p2)// Simplest: implement pretty.Stringer
type APIKey string
func (APIKey) PrettyString() string {
return "***REDACTED***"
}
// Or use errs.KeepSecret for function parameters
defer errs.WrapWithFuncParams(&err, errs.KeepSecret(apiKey))// 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))// 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)The go-errs-wrap command-line tool helps manage defer errs.WrapWithFuncParams statements in your Go code.
go install github.com/domonda/go-errs/cmd/go-errs-wrap@latest| 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 |
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.goRemove 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/...| Option | Description |
|---|---|
-out <path> |
Output to different location instead of modifying source |
-minvariadic |
Use specialized WrapWithNFuncParams functions instead of variadic |
-validate |
Dry run mode: check for issues without modifying files (useful for CI) |
-verbose |
Print progress information |
-help |
Show help message |
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
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.
- Go version: Requires Go 1.24+
- Error handling: Fully compatible with
errors.Is,errors.As,errors.Unwrap, anderrors.Join - Testing: Use with
testifyor any testing framework
- 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
See the godoc for more examples.
MIT License - see LICENSE file for details.
Contributions welcome! Please open an issue or submit a pull request.
- go-pretty - Pretty printing used for error parameter formatting