errors

package module
v0.0.0-...-9b8cb37 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2025 License: MIT Imports: 9 Imported by: 0

README

blugnu/errors

TL;DR: to get started, go get github.com/blugnu/errors (or later, if available)

A go package providing an ErrorWithContext implementation of the error interface, to wrap an error together with additional information including:

  • context.Context
  • a hint string
  • a stack trace
  • a cause error (or errors)
  • a timestamp
  • properties (key/value pairs)

Any or all of these may be used to enrich the error, providing additional context and information about the error that occurred.

A PanicError type is also provided to wrap a panic value, capturing the value that was recovered from the panic, a stack trace, and a timestamp.

In addition to the ErrorWithContext type, the package provides types to simplify error handling in scenarios involving consecutive or parallel operations that may fail, such as configuration, database operations, HTTP requests, etc.

  • Collector: a type that collects errors from multiple operations, consolidated into a single error, optionally skipping operations that follow any that have failed.

  • WaitGroup: a type that allows concurrent operations to be run, collecting errors (including panics) from those operations and returning them as a single error.

Tech Stack

blugnu/errors is built on the following main stack:

Full tech stack here

History

This module is a ground-up re-write of the previously released (and still available) blugnu/errors module. This new implementation incorporates the functionality of that previous module with a number of enhancements.

The API is significantly different, but the core functionality remains the same. The new module is intended to be a drop-in replacement for the standard library errors package, with the same functionality, supplemented by the additional functionality provided by the ErrorWithContext type and its associated functions.

Standard Library Functions

The following functions are provided to replace the standard library errors or fmt package functions.

function description use in place of ...
New(s) creates a new, unique error with a supplied string errors.New(s)
Errorf(ctx, format, args...) creates a new error using fmt.Errorf() given a format string and args fmt.Errorf(s, args...)
Join(err...) consolidate multiple errors errors.Join(err...)
Wrap(ctx, err...) creates a new error, wrapping a Context with one or two specified errors fmt.Errorf("%w: %w", err1, err2)
or
errors.Wrap(err1, err2)
example: New()
    if len(sql) == 0 {
        return errors.New("a sql statement is required")
    }
example: Errorf()
1. formatting a new error
    if len(pwd) < minpwdlen {
        return errors.Errorf("password must be at least %d chars", minpwdlen)
    }
2. adding narration to an existing error
    if err := db.QueryContext(ctx, sql, args); err != nil {
        return errors.Errorf("db query: %w", err)
    }
example: Join()

Wrapping an arbitrary collection of possibly nil errors:

    err1 := Operation1(ctx)
    err2 := Operation2(ctx)
    if err := errors.Join(err1, err2); err != nil {
        return err
    }
example: Wrap()
1. wrapping an existing error
    if err := db.QueryContext(ctx, sql, args); err != nil {
        return errorcontext.Wrap(ctx, err)
    }
2. wrapping two errors

When wrapping two (2) errors they are composed into an error: cause error chain, equivalent to Wrap(ctx, fmt.Errorf("%w: %w", err1, err2)); this is a useful pattern for attaching a sentinel error to some arbitrary error, typically to simplify testing:

    if err := db.QueryContext(ctx, sql, args); err != nil {
        return errorcontext.Wrap(ctx, ErrQueryFailed, err)
    }

A test of a function containing this code can check for the sentinel error without being coupled to details of the error returned by the function called by the function under test, for example:

    if err := Foo(ctx); !errors.Is(err, ErrQueryFailed) {
        t.Errorf("expected ErrQueryFailed, got %v", err)
    }

Working With Errors

TheContext captured by an ErrorWithContext may be obtained (if required) by determining whether an error is (or wraps) an ErrorWithContext. If an ErrorWithContext is available from an error the Context() function may then be called to obtain the Context associated with the error:

ctx := context.Background()

// ...

if err := Foo(ctx); err != nil {
    ctx := ctx // shadow ctx for the context associated with the error, if different from the current ctx
    ewc := ErrorWithContext{}
    if errors.As(err, &ewc) {
        ctx = ewc.Context()
    }
    // whether ctx is still the original or one captured from the error,
    // it is the most enriched context available and can be used to
    // initialize a context logger, for example
    log := logger.FromContext(ctx)
    log.Error(err)
}

The errorcontext.From() helper function provides a convenient way to do this, accepting a default Context (usually the current context) to use if no Context is captured by the error, simplifying the above to:

    if err := Foo(ctx); err != nil {
        ctx := errorcontext.From(ctx, err)
        log := logger.FromContext(ctx)
        log.Error(err)
    }

NOTE: The Context() function will recursively unwrap any further ErrorWithContext errors to return the Context associated with the most-wrapped error possible. This ensures that the most enriched Context that is available is returned.



Intended Use

ErrorWithContext is intended to reduce "chatter" when logging errors, particularly when using a context logger to enrich structured logs.

The Problem

  1. A Context enriched by a call hierarchy is most enriched at the deepest levels of a call hierarchy.
  2. Idiomatically wrapped errors provide the greatest narrative at the shallowest level of that call hierarchy.

This may be demonstrated with an example:

func Bar(ctx context.Context) error {
    return errors.New("not implemented")
}

func Foo(ctx context.Context, arg int) error {
    ctx := context.WithValue(ctx, fooKey, arg)
    if err := Bar(ctx, arg * 2); err != nil {
        return fmt.Errorf("Bar: %w", err)
    }
    return nil
}

func main() {
    ctx := context.Background()
    if err := Foo(ctx, 42); err != nil {
        log.Fatalf("Foo: %s", err)
    }
}

This produces the output:

FATAL message="Foo: Bar: not implemented"

The error string, as logged, describes the origin of the error.

However, the Context available at the point at which the error is logged contains none of the keys which might be used by a context logger to enrich a log entry with additional information not available in the error string.

If a context logger is used to log an error with that enrichment, deep within the call hierarchy, the error string lacks the additional narrative obtained by passing the error back up the call hierarchy. But if every function that receives an error does this then the log becomes very noisy and potentially confusing if context logging is not consistently used:

func Bar(ctx context.Context) error {
    log.Error("not implemented")
    return errors.New("not implemented")
}

func Foo(ctx context.Context, arg int) error {
    ctx := context.WithValue(ctx, fooKey, arg)
    if err := Bar(ctx, arg * 2); err != nil {
        log := logger.FromContext(ctx)
        log.Errorf("Bar: %s", err)
        return fmt.Errorf("Bar: %w", err)
    }
    return nil
}

func main() {
    ctx := context.Background()
    if err := Foo(ctx, 42); err != nil {
        log.Fatalf("Foo: %s", err)
    }
}

Which might produce log output similar to:

ERROR message="not implemented"
ERROR foo=42 message="Bar: not implemented"
FATAL message="Foo: Bar: not implemented"

there is a lot else wrong with the error handling and reporting in this example; it is intended only as an illustration and as such deliberately presents a potential worst case


ErrorWithContext addresses this problem by providing a mechanism for returning the context at each level back up the call hierarchy together with the error that occurred.

A simple convention then ensures that the error is logged only once and with the greatest possible context information available from the source of the error.

The convention has two parts:

  1. If an error is returned, it is not logged but returned as an ErrorWithContext (if a local Context is available), or at least returned without context

  2. If an error is not returned (usually at the effective or actual root of a call hierarchy, e.g. in a http handler) it is logged using a context logger initialized from context captured with the error (if any)

Informational and warning logs may of course continue to be emitted at every level in the call hierarchy.

Applying this convention to the previous example illustrates the benefits:

func Bar(ctx context.Context) error {
    log.Error("not implemented")
    return errorcontext.New(ctx, "not implemented")
}

func Foo(ctx context.Context, arg int) error {
    ctx := context.WithValue(ctx, fooKey, arg)
    if err := Bar(ctx, arg * 2); err != nil {
        return errorcontext.Errorf("Bar: %w", err)
    }
    return nil
}

func main() {
    ctx := context.Background()
    if err := Foo(ctx, 42); err != nil {
        ctx := errorcontext.From(err)
        log := logger.FromContext(ctx)
        log.Fatalf("Foo: %s", err)
    }
}

which might result in output similar to:

FATAL foo=42 message="Foo: Bar: not implemented"

Error handling is simplified and idiomatic, with the benefit of both fully enriched context logging and descriptive error messages.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// Is is an alias for [errors.Is] that checks if an error is equal to a target error.
	Is = errors.Is

	// Join is an alias for [errors.Join] that combines multiple errors into a single error.
	Join = errors.Join

	// New is an alias for [errors.New] that creates a new error with the
	// provided message.
	New = errors.New

	// Unwrap is an alias for [errors.Unwrap] that retrieves the next error in
	// the chain of wrapped errors.
	Unwrap = errors.Unwrap
)
View Source
var (
	// ErrUnsupported is an alias for [errors.ErrUnsupported], indicating that an operation
	// is not supported.
	ErrUnsupported = errors.ErrUnsupported
)

Functions

func As

func As[T error](err error) (T, bool)

As replaces the default errors.As function to provide a more convenient mechanism for checking if an error can be cast to a specific error type, removing the need to pre-declare the target variable.

The function returns a value of type T and a boolean indicating whether the cast was successful. If the cast fails, the zero value of T is returned.

func Caller

func Caller() func(*ErrorWithContext)

Caller is a property function that can be used to attach the caller's package and function name to an ErrorWithContext. This is useful for debugging and logging purposes, as it provides information about where the error was created.

func Cause

func Cause(errs ...error) func(*ErrorWithContext)

Cause is a property function that can be used to attach one or more errors to an ErrorWithContext. An error cause is attached by wrapping the existing error with the provided errors.

If no errors are provided, no cause will be set and the existing error will remain unchanged.

If more than one error is provided, they are joined together using errors.Join and the resulting error used as the cause.

Multiple Cause(err) vs Cause(.. multiple errors ..)

If multiple Cause functions are specified, each is applied in turn, according to the rules above, in the order specified.

e.g. `Cause(a), Cause(b)` when applied to an error `err`, will first join `a` with `err` and then join `b` to that combined error, resulting in `err: a: b`.

By contrast, `Cause(a, b)` will first join `a` and `b` to provide a single cause to be joined with the error `err`, resulting in `err:\na\b`.

func Context

func Context(ctx context.Context) func(*ErrorWithContext)

Context is a property function that can be used to attach a context to an ErrorWithContext. The context can be used to provide additional information about the error, such as the request context or any other relevant context.

If the context is not nil, it will be set on the ErrorWithContext and will be returned by the ErrorWithContext.Context method. A non-nil context.Context is also used to set a ErrorWithContext.Timestamp for the error, using a time.Clock from the context.

If the context is nil, it will not be set on the ErrorWithContext and the existing context (if any) will remain unchanged.

func ContextFrom

func ContextFrom(err error, defaultContext ...context.Context) context.Context

ContextFrom returns the context.Context from the 'most wrapped' ErrorWithContext that is obtained by unwrapping the error chain from a specified error.

A default context may be provided which will be returned if the error is not an ErrorWithContext (and does not wrap one). If a context is not available in the error chain and no default context is provided, nil is returned.

The default context is a variadic parameter so that it can be omitted if not needed. If more than one default context is specified only the first is used.

func Errorf

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

Errorf creates a new error with a formatted message.

When called with no arguments, it behaves like errors.New and returns a simple error with the provided format string.

When called with arguments, it behaves like fmt.Errorf and formats the format string with the provided arguments.

func Hint

func Hint(hint string) func(*ErrorWithContext)

Hint is a property function that can be used to attach a hint to an ErrorWithContext. The hint can be used to provide additional context or information about the error.

If the hint is an empty string, it will not be set on the ErrorWithContext and the existing hint (if any) will remain unchanged.

The hint is not used by the ErrorWithContext methods nor is it included in the ErrorWithContext.Error string.

func NewPanicError

func NewPanicError(r any) error

NewPanicError creates a new PanicError with the given recovered value and captures the stack trace at the point of panic. This is useful for creating a PanicError when a panic is recovered in a goroutine or function.

If the recovered value is nil, nil is returned.

func P

func P(key string, value any) func(*ErrorWithContext)

P is a property function that can be used to attach properties to an ErrorWithContext.

If the key is an empty string, the existing properties (if any) will remain unchanged.

func Properties

func Properties(props map[string]any) func(*ErrorWithContext)

Properties is a property function that can be used to attach multiple properties to an ErrorWithContext by providing a map of properties to be combined with any existing properties.

If the map is nil or empty, the existing properties (if any) will remain unchanged.

func StackTrace

func StackTrace() func(*ErrorWithContext)

StackTrace is a property function that can be used to attach a stack trace to an ErrorWithContext. The stack trace is captured at the point the error is created and can be useful for debugging purposes.

If the error already has a stack trace it is not over-written.

func Timestamp

func Timestamp(ctx context.Context) func(*ErrorWithContext)

Timestamp is a property function that can be used to set the UTC timestamp of an ErrorWithContext. The timestamp is the time at which the error was created and can be useful for logging and debugging purposes.

A context.Context is used to obtain a time.Clock from which the time is obtained. If the context does not contain a clock, the system clock is used.

func WithCaller

func WithCaller(err error, props ...ErrorProperty) error

WithCaller creates a new error that wraps an original error with the name of the function where the error was created.

func WithCause

func WithCause(err, cause error, props ...ErrorProperty) error

WithCause creates a new error that wraps an original error with an additional cause. Additional properties may optionally be applied to the error using property functions.

  • If both `err` and `cause` are nil, it returns nil.
  • If `err` is nil, it returns `cause`.
  • If `cause` is nil, it returns `err`.
  • If both `err` and `cause` are non-nil, it returns a new error that wraps `err` and `cause` using `fmt.Errorf` with the format "%w: %w".

func WithContext

func WithContext(ctx context.Context, err error, props ...ErrorProperty) error

WithContext creates a new error that includes a context and an original error. The supplied context is also used to set a timestamp for the error (see WithTimestamp).

If `err` is nil, the function returns nil.

func WithHint

func WithHint(err error, hint string, props ...ErrorProperty) error

WithHint creates a new error that wraps an original error with a hint. A hint is a short message that provides additional context or guidance about the error.

If `err` is nil, the function returns nil.

To wrap an error with other information in addition to a hint, use the [Wrap] function with the required property functions.

func WithProperties

func WithProperties(err error, props map[string]any) error

WithProperties creates a new error that wraps an original error with properties from a provided map.

If `err` is nil, the function returns nil.

If no `props` are provided, the original error is returned.

func WithProperty

func WithProperty(err error, key string, value any) error

WithProperty creates a new error that wraps an original error with a single property.

If `err` is nil, the function returns nil.

func WithStackTrace

func WithStackTrace(err error, props ...ErrorProperty) error

WithStackTrace creates a new error that wraps an original error with a stack trace. The stack trace is captured at the point where the error is created and can be useful for debugging purposes.

If `err` is nil, the function returns nil.

func WithTimestamp

func WithTimestamp(ctx context.Context, err error, props ...ErrorProperty) error

WithTimestamp creates a new error that wraps an original error with a timestamp. The timestamp is set to the current UTC time when the error is created.

A context.Context is required and is used to obtain a time.Clock from which the time is obtained, enabling the injection of a mock clock for testing. If the context does not contain a clock, the system clock is used.

The supplied context is also set on the ErrorWithContext and can be retrieved using the Context method.

If `err` is nil, the function returns nil.

Types

type Collector

type Collector struct {
	sync.Mutex
	// contains filtered or unexported fields
}

Collector is a thread-safe error collector that allows you to collect errors from multiple functions and retrieve them as a single error.

The functions performed by the collector can be run conditionally based on whether the collector already has errors or not or may be run regardless of existing errors.

This is useful for simplifying error handling in scenarios where multiple operations may fail, and you want to collect all errors without stopping at the first one or where subsequent operations depend on the success of previous ones.

Example Usage

c := &errors.Collector{}

c.Try("operation1", func() error {
    // perform operation
    return nil // or an error
})

c.Try("operation2", func() error {
    // perform another operation only if the first completed without error
    return errors.New("something went wrong")
})

c.MustTry("operation3", func() error {
    // perform yet another operation that must be performed even if previous operations
    // failed with errors
    return nil // or an error
})

return c.Error()

func NewCollector

func NewCollector() Collector

func (*Collector) Error

func (c *Collector) Error() error

Errors returns an error that wraps all collected errors.

The returned error is the result of errors.Join over all collected errors. If there are no errors, it returns nil.

func (*Collector) HasErrors

func (c *Collector) HasErrors() bool

HasErrors checks if the collector has any errors and returns

func (*Collector) MustTry

func (c *Collector) MustTry(name string, fn func() error)

MustTry runs the function and appends any error to the Collector.

The function is always performed, even if the Collector already has errors.

To perform a function only if the collector has no errors, use Collector.Try.

func (*Collector) MustTryAll

func (c *Collector) MustTryAll(fns ...func() error) error

MustTryAll runs each function in fns and appends any errors to the Collector.

All functions are performed and all errors are collected, even if the Collector already has errors or if any of the functions return an error.

To perform functions only if the collector has no errors, use Collector.TryAll.

func (*Collector) Try

func (c *Collector) Try(name string, fn func() error)

Try runs the function and appends any error to the Collector.

If the Collector already has errors, the function is not performed and no error is collected.

To perform a function even if the Collector has existing errors, use Collector.MustTry.

func (*Collector) TryAll

func (c *Collector) TryAll(fns ...func() error) error

TryAll runs each function in fns and appends any error to the Collector.

If any function returns an error, the Collector stops running the functions and returns the collected errors as a joined error.

To run all functions regardless of errors, use Collector.MustTryAll.

type ErrorProperty

type ErrorProperty func(*ErrorWithContext)

type ErrorWithContext

type ErrorWithContext struct {
	// contains filtered or unexported fields
}

ErrorWithContext wraps an error with a context and optional properties.

It implements the error interface and provides methods to access the context, properties, and the wrapped error. It is used to provide additional context and metadata about errors in a structured way.

func UnwrapContext

func UnwrapContext(err error) []ErrorWithContext

UnwrapContext extracts all ErrorWithContext errors in the error chain obtained from a specified error and returns them as a slice. The slice will contain all contexts that are wrapped in the error chain in depth first order (starting from the outermost error).

If the error is not an ErrorWithContext (and does not wrap any) a nil slice is returned.

func (ErrorWithContext) Context

func (ec ErrorWithContext) Context() context.Context

Context returns the inner-most context accessible from this error.

That is, the wrapped error is first tested to determine if it is (or wraps) an ErrorWithContext. If it is, then the ErrorWithContext.Context method on that wrapped error is called, which recursively unwraps the error until it finds the innermost context.

If the wrapped error is not an ErrorWithContext, then the context associated with the receiver is returned.

func (ErrorWithContext) Error

func (ec ErrorWithContext) Error() string

Error implements the error interface.

Attached properties are NOT included in the error message, which consists only of the error itself. It is expected that error handling code is implemented that is aware of properties, which are obtained using the UnwrapContext function to extract error and property information to be added to log entries or other error handling mechanisms, as required.

func (ErrorWithContext) HasProperties

func (ec ErrorWithContext) HasProperties() bool

func (ErrorWithContext) Hint

func (ec ErrorWithContext) Hint() string

Hint returns the hint associated with the error (if any).

func (ErrorWithContext) Is

func (ec ErrorWithContext) Is(target error) bool

Is compares an ErrorWithContext with some target error to determine whether they are considered equal.

A receiver will match with a target that:

  • is an ErrorWithContext; and
  • has an equal or nil context; and
  • has a nil error or an error satisfying errors.Is

Any properties attached to the ErrorWithContext are not considered in the comparison.

func (ErrorWithContext) Properties

func (ec ErrorWithContext) Properties() map[string]any

Properties returns a copy of any properties attached to the error. If there are no properties, nil is returned.

func (ErrorWithContext) Stack

func (ec ErrorWithContext) Stack() []byte

Stack returns the stack trace associated with the error, if any.

If no stack trace was captured, nil is returned.

If the error wraps a PanicError, the stack trace identifies the location of the panic, otherwise it reflects the point at which the error was created.

func (ErrorWithContext) Timestamp

func (ec ErrorWithContext) Timestamp() time.Time

Timestamp returns the time at which the error was created. If no timestamp was provided, the zero time is returned.

func (ErrorWithContext) Unwrap

func (ec ErrorWithContext) Unwrap() error

Unwrap returns the error wrapped by the ErrorWithContext.

type PanicError

type PanicError struct {
	// contains filtered or unexported fields
}

PanicError is an error type that represents a panic that has been recovered. It contains the recovered value and the stack trace at the point of panic.

func (PanicError) Error

func (e PanicError) Error() string

Error implements the error interface for PanicError.

func (PanicError) Is

func (e PanicError) Is(target error) bool

Is checks if the PanicError matches the target error. The `target` error is considered a match if it is a PanicError and:

  • the target and receiver have recovered errors and the target error satisfies errors.Is with respect to the receiver error; or
  • the target and receiver have non-nil recovered values that are equal; or
  • the target has no recovered value (nil)

func (PanicError) Unwrap

func (e PanicError) Unwrap() error

type WaitGroup

type WaitGroup struct {
	sync.WaitGroup
	sync.Once
	// contains filtered or unexported fields
}

WaitGroup is a wait group that allows for error collection from goroutines.

Each function run with WaitGroup.Go is expected to accept a context and to return an error. If any goroutine performed in the WaitGroup panics, the panic is recovered and a PanicError collected with the recovered value and stack trace leading to the panic.

All goroutines receive the same context, provided when the WaitGroup is created.

Non-nil errors returned by any goroutines are collected and returned by the WaitGroup.Wait method.

A WaitGroup is re-usable. Once WaitGroup.Wait has returned, the WaitGroup can be reused to run more goroutines. The WaitGroup will create a new channel to collect errors for any new goroutines.

func NewWaitGroup

func NewWaitGroup() WaitGroup

NewWaitGroup returns a new WaitGroup instance.

func (*WaitGroup) Go

func (wg *WaitGroup) Go(name string, fn func() error)

Go runs a provided function in a goroutine. If the function returns an error it is collected to be retrieved once the WaitGroup has completed, using the [WaitGroup.Error] method.

If the function panics, the panic is recovered and a PanicError is collected with the panic reason.

func (*WaitGroup) Wait

func (wg *WaitGroup) Wait() error

Wait waits for all goroutines to finish and collects any errors that were returned by the functions run in the WaitGroup as follows:

  • if no errors are collected, the function returns nil;
  • if a single error is collected, it is returned;
  • if 2 or more errors are collected, they are returned as a single error using errors.Join.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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