xerrors

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 12, 2026 License: MIT Imports: 7 Imported by: 36

README

go-xerrors

Go Reference Coverage Status

go-xerrors is a small, idiomatic library that makes error handling in Go easier. It provides utilities for creating errors with stack traces, wrapping existing errors, aggregating multiple errors, and recovering from panics.

Main features

  • Stack traces: Capture stack traces when creating errors to pinpoint the source during debugging.
  • Multi-errors: Aggregate multiple errors into a single error while preserving individual context.
  • Panic handling: Convert panic values into standard Go errors with stack traces.
  • Zero dependencies: No external dependencies beyond the Go standard library.

Note: This package is stable. Since 1.0 the API has been frozen, so no breaking changes will be introduced in future releases. Updates are rare and limited to bug fixes and support for new Go versions or error-related features.


Installation

go get -u github.com/mdobak/go-xerrors

Usage

Example

Here is a quick example of creating and handling errors with go-xerrors.

package main

import (
    "database/sql"
    "fmt"

    "github.com/mdobak/go-xerrors"
)

func findUserByID(id int) error {
    // Simulate a standard library error.
    err := sql.ErrNoRows

    // Wrap the original error with additional context and capture a stack trace
    // at this point in the call stack.
    return xerrors.Newf("user %d not found: %w", id, err)
}

func main() {
    err := findUserByID(123)
    if err != nil {
        // 1) err.Error() returns a concise, log-friendly message.
        fmt.Println(err.Error())
        // Output:
        // user 123 not found: sql: no rows in result set

        // 2) xerrors.Print writes a detailed message with a stack trace.
        xerrors.Print(err)
        // Output:
        // Error: user 123 not found: sql: no rows in result set
        //     at main.findUserByID (/home/user/app/main.go:16)
        //     at main.main (/home/user/app/main.go:20)
        //     at runtime.main (/usr/local/go/src/runtime/proc.go:250)
        //     at runtime.goexit (/usr/local/go/src/runtime/asm_amd64.s:1594)
    }
}
Creating Errors with Stack Traces

The primary way to create an error in go-xerrors is by using the xerrors.New or xerrors.Newf functions:

// Create a new error with a stack trace.
err := xerrors.New("something went wrong")

// Create a formatted error with a stack trace.
err = xerrors.Newf("something went wrong: %s", reason)

Calling err.Error() returns only the message, such as something went wrong, following the Go convention of keeping error strings concise. The stack trace is kept separately and is printed only when you ask for it.

Displaying Detailed Errors

To print an error together with its stack trace and other details, use xerrors.Print, xerrors.Sprint, or xerrors.Fprint:

xerrors.Print(err)

Output:

Error: something went wrong
	at main.main (/home/user/app/main.go:10)
	at runtime.main (/usr/local/go/src/runtime/proc.go:225)
	at runtime.goexit (/usr/local/go/src/runtime/asm_amd64.s:1371)
Working with Stack Traces

To retrieve the stack trace programmatically:

trace := xerrors.StackTrace(err)
fmt.Print(trace)

Output:

at main.main (/home/user/app/main.go:10)
at runtime.main (/usr/local/go/src/runtime/proc.go:225)
at runtime.goexit (/usr/local/go/src/runtime/asm_amd64.s:1371)

xerrors.StackTrace returns a xerrors.Callers value, which is a list of program counters. Call its Frames method to get the file, line, and function of each frame. If the error carries no stack trace, the returned value is empty.

You can also add a stack trace to an existing error with xerrors.WithStackTrace and choose how many frames to skip. This is handy when a helper creates errors but you do not want the helper's own frame to appear at the top of the trace:

func errNotFound(path string) error {
	// Skip one frame so that the stack trace starts at the caller of
	// errNotFound rather than at errNotFound itself.
	return xerrors.WithStackTrace(&NotFoundError{Path: path}, 1)
}
Wrapping Errors

You can also wrap existing errors:

output, err := json.Marshal(data)
if err != nil {
	return xerrors.New("failed to marshal data", err)
}

With formatted messages:

output, err := json.Marshal(data)
if err != nil {
	return xerrors.Newf("failed to marshal data %v: %w", data, err)
}

Wrapping more than one error with a single xerrors.Newf call requires Go 1.20 or later.

Creating Error Chains Without Stack Traces

When you do not need a stack trace, for example when creating sentinel errors, use xerrors.Join and xerrors.Joinf:

err := xerrors.Join("operation failed", otherErr)

With formatted messages:

err := xerrors.Joinf("operation failed: %w", otherErr)

Wrapping more than one error with a single xerrors.Joinf call requires Go 1.20 or later.

The main difference between Go's fmt.Errorf and xerrors.Newf / xerrors.Joinf is that the latter preserve the error chain, whereas fmt.Errorf flattens it. In other words, Unwrap on an error created by go-xerrors returns the next error in the chain, while fmt.Errorf returns all wrapped errors at once.

Sentinel Errors

Sentinel errors are predefined, exported error values used to signal specific, well-known conditions, such as io.EOF. The go-xerrors package provides the xerrors.Message and xerrors.Messagef functions to create distinct sentinel error values:

var ErrAccessDenied = xerrors.Message("access denied")

// ...

func performAction() error {
	// ...
	return ErrAccessDenied
}

// ...

err := performAction()
if errors.Is(err, ErrAccessDenied) {
	log.Println("Operation failed due to access denial.")
}

For formatted sentinel errors:

const MaxLength = 10

var ErrInvalidInput = xerrors.Messagef("max length of %d exceeded", MaxLength)

Every call returns a distinct error value, even when the message is the same, so two sentinel errors are never accidentally equal.

Multi-Errors

When performing multiple independent operations where several might fail, use xerrors.Append to collect the individual errors into a single multi-error:

var err error

if input.Username == "" {
	err = xerrors.Append(err, xerrors.New("username cannot be empty"))
}
if len(input.Password) < 8 {
	err = xerrors.Append(err, xerrors.New("password must be at least 8 characters"))
}

if err != nil {
	fmt.Println(err.Error())
	// Output:
	// [username cannot be empty, password must be at least 8 characters]

	// Detailed output using xerrors.Print:
	xerrors.Print(err)
	// Output:
	// Error: [username cannot be empty, password must be at least 8 characters]
	//     1. Error: username cannot be empty
	//         at main.validateInput (/home/user/app/main.go:40)
	//         at main.main (/home/user/app/main.go:20)
	//         at runtime.main (/usr/local/go/src/runtime/proc.go:250)
	//         at runtime.goexit (/usr/local/go/src/runtime/asm_amd64.s:1594)
	//     2. Error: password must be at least 8 characters
	//         at main.validateInput (/home/user/app/main.go:43)
	//         at main.main (/home/user/app/main.go:20)
	//         at runtime.main (/usr/local/go/src/runtime/proc.go:250)
	//         at runtime.goexit (/usr/local/go/src/runtime/asm_amd64.s:1594)
}

xerrors.Append never modifies the error passed to it, so the same error can safely be appended to more than once. If all errors are nil, it returns nil; if only one error remains, it returns that error instead of a list.

The resulting multi-error implements the standard error interface, as well as errors.Is, errors.As, and the Go 1.20 Unwrap() []error method, so you can check for specific errors or extract them.

Comparison with Go 1.20 errors.Join:

Go 1.20 introduced errors.Join for error aggregation. While it serves a similar purpose, xerrors.Append preserves the individual stack traces associated with each appended error and keeps the Error() method to a single line.

Simplified Panic Handling

Panics can be difficult to locate and handle effectively in Go applications, especially when using recover(). Common issues, such as nil pointer dereferences or out-of-bounds slice accesses, often result in unclear panic messages, and without a stack trace, pinpointing the origin of a panic can be difficult.

go-xerrors provides utilities that convert panic values into proper errors with stack traces.

Using xerrors.Recover:

func handleTask() (err error) {
	defer xerrors.Recover(func(err error) {
		log.Printf("Recovered from panic during task handling: %s", xerrors.Sprint(err))
	})

	// ... potentially panicking code ...

	return nil
}

xerrors.Recover must be used directly with the defer keyword, and the callback is invoked only when a panic actually occurs.

Using xerrors.FromRecover:

func handleTask() (err error) {
	defer func() {
		if r := recover(); r != nil {
			// Convert the recovered value into an error with a stack trace.
			err = xerrors.FromRecover(r)
			log.Printf("Recovered from panic during task handling: %s", xerrors.Sprint(err))
		}
	}()

	// ... potentially panicking code ...

	return nil
}

xerrors.FromRecover must be called in the same function as recover(), otherwise the stack trace will not point to the origin of the panic.

In both cases the returned error implements the PanicError interface, which provides access to the original panic value via the Panic() method.

Choosing Between New, Join, and Append

All three functions can combine errors, but each serves a distinct purpose:

  • xerrors.New: Use it to create errors and attach stack traces, especially when wrapping existing errors to provide additional context.
  • xerrors.Join: Use it to chain errors together without capturing a stack trace.
  • xerrors.Append: Use it to aggregate multiple independent errors into a single multi-error. This is useful when several operations might fail and you want to report all failures at once.
Examples
Error with Stack Trace
func (m *MyStruct) MarshalJSON() ([]byte, error) {
	output, err := json.Marshal(m)
	if err != nil {
		// Wrap the error with additional context and capture a stack trace.
		return nil, xerrors.New("failed to marshal data", err)
	}
	return output, nil
}
Sentinel Errors
var (
	// Using xerrors.Join allows us to create sentinel errors that can be
	// checked with errors.Is against both ErrValidation and the specific
	// validation error. We do not want to capture a stack trace here,
	// therefore we use xerrors.Join instead of xerrors.New.
	ErrValidation   = xerrors.Message("validation error")
	ErrInvalidName  = xerrors.Join(ErrValidation, "name is invalid")
	ErrInvalidAge   = xerrors.Join(ErrValidation, "age is invalid")
	ErrInvalidEmail = xerrors.Join(ErrValidation, "email is invalid")
)

func (m *MyStruct) Validate() error {
	if !m.isNameValid() {
		return xerrors.New(ErrInvalidName)
	}
	if !m.isAgeValid() {
		return xerrors.New(ErrInvalidAge)
	}
	if !m.isEmailValid() {
		return xerrors.New(ErrInvalidEmail)
	}
	return nil
}
Multi-Error Validation
func (m *MyStruct) Validate() error {
	var err error
	if m.Name == "" {
		err = xerrors.Append(err, xerrors.New("name cannot be empty"))
	}
	if m.Age < 0 {
		err = xerrors.Append(err, xerrors.New("age cannot be negative"))
	}
	if m.Email == "" {
		err = xerrors.Append(err, xerrors.New("email cannot be empty"))
	}
	return err
}

API Reference

Core Functions
  • xerrors.New(vals ...any) error: Creates an error with a stack trace
  • xerrors.Newf(format string, args ...any) error: Creates a formatted error with a stack trace
  • xerrors.Join(vals ...any) error: Creates a chained error without a stack trace
  • xerrors.Joinf(format string, args ...any) error: Creates a formatted chained error without a stack trace
  • xerrors.Message(msg string) error: Creates a simple sentinel error
  • xerrors.Messagef(format string, args ...any) error: Creates a formatted sentinel error
  • xerrors.Append(err error, errs ...error) error: Aggregates errors into a multi-error
Panics
  • xerrors.Recover(fn func(err error)): Recovers from a panic and invokes the callback with the resulting error
  • xerrors.FromRecover(r any) error: Converts a recovered value into an error with a stack trace
Stack Traces
  • xerrors.StackTrace(err error) Callers: Extracts the stack trace from an error
  • xerrors.WithStackTrace(err error, skip int) error: Wraps an error with a stack trace, skipping skip frames
  • xerrors.Callers: A stack trace, represented as a list of program counters
  • xerrors.Frame: A single stack frame, with its file, line, and function
  • xerrors.DefaultCallersFormatter: The default formatter for Callers, used when printing stack traces
  • xerrors.DefaultFrameFormatter: The default formatter for Frame, used when printing stack traces
Error Printing
  • xerrors.Print(err error): Writes a formatted error to stderr
  • xerrors.Sprint(err error) string: Returns a formatted error as a string
  • xerrors.Fprint(w io.Writer, err error) (int, error): Writes a formatted error to the provided writer
Interfaces
  • xerrors.DetailedError: For errors that provide details beyond the error message, such as a stack trace
  • xerrors.PanicError: For errors created from panic values, with access to the original panic value

Documentation

For full API details, see the documentation:

https://pkg.go.dev/github.com/mdobak/go-xerrors

License

Licensed under the MIT License.

Documentation

Overview

Package xerrors is a small, idiomatic library that makes error handling in Go easier. It provides utilities for creating errors with stack traces, wrapping existing errors, aggregating multiple errors, and recovering from panics.

Index

Constants

This section is empty.

Variables

View Source
var DefaultCallersFormatter = func(c Callers, w io.Writer) {
	for _, frame := range c.Frames() {
		io.WriteString(w, "at ")
		frame.writeFrame(w)
		io.WriteString(w, "\n")
	}
}

DefaultCallersFormatter is the default formatter for Callers.

View Source
var DefaultFrameFormatter = func(f Frame, w io.Writer) {
	io.WriteString(w, shortname(f.Function))
	io.WriteString(w, " (")
	io.WriteString(w, f.File)
	io.WriteString(w, ":")
	io.WriteString(w, strconv.Itoa(f.Line))
	io.WriteString(w, ")")
}

DefaultFrameFormatter is the default formatter for Frame.

Functions

func Append

func Append(err error, errs ...error) error

Append appends the provided errors to an existing error or list of errors. If err is not a multi-error, it is converted into one. Nil errors are ignored. It does not record a stack trace.

The errors passed as arguments are not modified.

If the resulting error list is empty, nil is returned. If the resulting error list contains only one error, that error is returned instead of the list.

The returned error is compatible with Go errors, supporting errors.Is, errors.As, and the Go 1.20 `Unwrap() []error` method.

To create a chained error, use New, Newf, Join, or Joinf instead.

func Fprint

func Fprint(w io.Writer, err error) (int, error)

Fprint writes a formatted error to the provided io.Writer. It returns the number of bytes written and any error encountered while writing.

If any error in the chain implements DetailedError and returns a non-empty string, its details are appended to the error message.

If err is nil, nothing is written.

func FromRecover

func FromRecover(r any) error

FromRecover converts the result of the built-in `recover()` into an error with a stack trace. The returned error implements PanicError. It returns nil if `r` is nil.

This function must be called in the same function as `recover()` to ensure that the stack trace is accurate.

func Join added in v1.0.0

func Join(vals ...any) error

Join joins multiple values into a single error, forming a chain of errors. Each value wraps the one that follows it.

Conversion rules for arguments:

  • If the value is an error, it is used as is.
  • If the value is a string, a new error with that message is created.
  • If the value implements fmt.Stringer, the result of String() is used to create an error.
  • If the value is nil, it is ignored.
  • Otherwise, the result of fmt.Sprint is used to create an error.

If called with no arguments or only nil values, Join returns nil.

To create a multi-error instead of an error chain, use Append.

func Joinf added in v1.0.0

func Joinf(format string, args ...any) error

Joinf joins multiple values into a single error with a formatted message, forming an error chain. The format string follows the conventions of fmt.Errorf.

Unlike errors created by fmt.Errorf, the Unwrap method on the returned error yields the next wrapped error, not a slice of errors, because this function is intended for creating linear error chains.

To create a multi-error instead of an error chain, use Append.

func Message

func Message(msg string) error

Message creates a simple error with the given message, without recording a stack trace. Each call returns a distinct error instance, even if the message is identical.

This function is useful for creating sentinel errors, often referred to as "constant errors."

To create an error with a stack trace, use New or Newf instead.

func Messagef added in v1.0.0

func Messagef(format string, args ...any) error

Messagef creates a simple error with a formatted message, without recording a stack trace. The format string follows the conventions of fmt.Sprintf. Each call returns a distinct error instance, even if the message is identical.

This function is useful for creating sentinel errors, often referred to as "constant errors."

To create an error with a stack trace, use New or Newf instead.

func New

func New(vals ...any) error

New creates a new error from the provided values and records a stack trace at the point of the call. If multiple values are provided, each value wraps the one that follows it, forming a chain of errors.

Usage examples:

  • Add a stack trace to an existing error: New(err)
  • Create an error with a message and a stack trace: New("access denied")
  • Wrap an error with a message: New("access denied", io.EOF)
  • Add context to a sentinel error: New(ErrRead, "access denied")

Conversion rules for arguments:

  • If the value is an error, it is used as is.
  • If the value is a string, a new error with that message is created.
  • If the value implements fmt.Stringer, the result of String() is used to create an error.
  • If the value is nil, it is ignored.
  • Otherwise, the result of fmt.Sprint is used to create an error.

If called with no arguments or only nil values, New returns nil.

To create a sentinel error, use Message or Messagef instead.

func Newf added in v1.0.0

func Newf(format string, args ...any) error

Newf creates a new error with a formatted message and records a stack trace at the point of the call. The format string follows the conventions of fmt.Errorf.

Unlike errors created by fmt.Errorf, the Unwrap method on the returned error yields the next wrapped error, not a slice of errors, because this function is intended for creating linear error chains.

To create a sentinel error, use Message or Messagef instead.

func Print

func Print(err error)

Print writes a formatted error to stderr.

If any error in the chain implements DetailedError and returns a non-empty string, its details are appended to the error message.

If err is nil, nothing is written.

func Recover

func Recover(fn func(err error))

Recover wraps the built-in `recover()` function, converting the recovered value into an error with a stack trace. The provided `fn` callback is only invoked when a panic occurs.

This function must always be used directly with the `defer` keyword.

func Sprint

func Sprint(err error) string

Sprint returns a formatted error as a string.

If any error in the chain implements DetailedError and returns a non-empty string, its details are appended to the error message.

The output may span multiple lines and ends with a newline. If err is nil, an empty string is returned.

func WithStackTrace

func WithStackTrace(err error, skip int) error

WithStackTrace wraps the provided error with a stack trace, capturing the stack at the point of the call. The `skip` argument specifies how many stack frames to skip: 0 starts the stack trace at the caller of WithStackTrace, 1 at its caller, and so on.

If err is nil, WithStackTrace returns nil.

Types

type Callers

type Callers []uintptr

Callers is a stack trace represented as a list of program counters, as returned by runtime.Callers.

func StackTrace

func StackTrace(err error) Callers

StackTrace extracts the stack trace from the provided error. It traverses the error chain and returns the stack trace of the innermost error that has one. It returns nil if no error in the chain has a stack trace.

func (Callers) Format

func (c Callers) Format(s fmt.State, verb rune)

Format implements the fmt.Formatter interface.

Supported verbs:

  • %s the complete stack trace, one frame per line
  • %v same as %s; the '+' and '#' flags print the raw program counters
  • %q the result of %s as a double-quoted Go string

func (Callers) Frames

func (c Callers) Frames() []Frame

Frames returns a slice of Frame structs with function, file, and line information. It returns nil if the stack trace is empty.

func (Callers) String

func (c Callers) String() string

String implements the fmt.Stringer interface.

type DetailedError

type DetailedError interface {
	error

	// ErrorDetails returns additional details about the error. It should not
	// repeat the error message and should end with a newline.
	//
	// An empty string is returned if the error does not provide
	// additional details.
	ErrorDetails() string
}

DetailedError represents an error that provides additional details beyond the error message.

type Frame

type Frame struct {
	File     string
	Line     int
	Function string
}

Frame represents a single stack frame with file, line, and function details.

func (Frame) Format

func (f Frame) Format(s fmt.State, verb rune)

Format implements the fmt.Formatter interface.

Supported verbs:

  • %s function, file, and line number on a single line
  • %f file path
  • %d line number
  • %n function name; the '+' flag prints the package path as well
  • %v same as %s; the '+' and '#' flags print the struct fields
  • %q the result of %s as a double-quoted Go string

func (Frame) String

func (f Frame) String() string

String implements the fmt.Stringer interface.

type PanicError added in v1.0.0

type PanicError interface {
	error

	// Panic returns the raw panic value.
	Panic() any
}

PanicError represents an error that occurs during a panic. It is implemented by the errors returned by the Recover and FromRecover functions. It provides access to the original panic value via the Panic method.

Use the standard errors.As function to convert an error to this interface.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL