wrap

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2025 License: MIT Imports: 3 Imported by: 13

README

wrap

A small Go library for wrapping errors with extra context.

Run go get hermannm.dev/wrap to add it to your project!

Docs: pkg.go.dev/hermannm.dev/wrap

Contents:

Motivation behind the library

Go's fmt.Errorf is a great way to provide context to errors before returning them. However, the common way of using fmt.Errorf("extra context: %w", err) can lead to long and hard-to-read error messages. See the following example, albeit a bit contrived:

event processing failed: failed to store event: database insert failed: duplicate primary key 

This library's wrap.Error aims to alleviate this, by instead formatting wrapped errors like this:

event processing failed
- failed to store event
- database insert failed
- duplicate primary key

The library also provides:

  • wrap.Errorf to use a format string for the wrapping message
  • wrap.Errors to wrap multiple errors
  • wrap.ErrorWithAttrs to attach structured log attributes (from the standard log/slog package), to provide better context when an error is logged
    • The error returned by this wrapper can be used by a logging library (such as hermannm.dev/devlog/log) to add the error attributes to the log output
  • A ctxwrap subpackage to attach context.Context to errors (see below for more on this)

Usage

Basic usage:

err := errors.New("duplicate primary key")
wrapped := wrap.Error(err, "database insert failed")
fmt.Println(wrapped)
// database insert failed
// - duplicate primary key

Wrapped errors can be nested. Wrapping an already wrapped error adds it to the error list, as follows:

wrapped2 := wrap.Error(wrapped, "failed to store event")
fmt.Println(wrapped2)
// failed to store event
// - database insert failed
// - duplicate primary key

wrap.Errorf can be used to create the wrapping message with a format string:

err := errors.New("unrecognized event type")
wrapped := wrap.Errorf(err, "failed to process event of type '%s'", "ORDER_UPDATED")
fmt.Println(wrapped)
// failed to process event of type 'ORDER_UPDATED'
// - unrecognized event type

...and wrap.Errors can be used to wrap multiple errors:

errs := []error{errors.New("invalid timestamp format"), errors.New("id was not UUID")}
wrapped := wrap.Errors(errs, "failed to parse event")
fmt.Println(wrapped)
// failed to parse event
// - invalid timestamp format
// - id was not UUID

When combining wrap.Errors and wrap.Error, nested errors are indented as follows:

errs := []error{errors.New("invalid timestamp format"), errors.New("id was not UUID")}
inner := wrap.Errors(errs, "failed to parse event")
outer := wrap.Error(inner, "event processing failed")
fmt.Println(outer)
// event processing failed
// - failed to parse event
//   - invalid timestamp format
//   - id was not UUID

Finally, wrap.ErrorWithAttrs lets you attach structured log attributes to errors. This can be used by error-aware logging libraries, such as hermannm.dev/devlog/log, to add the error's attributes to the log output.

func example() error {
	req := ExternalServiceRequest { /* ... */ }
	resp, err := callExternalService(request)
	if != nil {
		// When this error is logged by a compatible logging library (like devlog/log),
		// the log output will have a "request" attribute with the given struct
		return wrap.ErrorWithAttrs(err, "Request to external service failed", "request", req) 
	}
}

The ctxwrap subpackage

This library also provides a ctxwrap subpackage, which tries to solve the problem of errors escaping their context. It mirrors the API of wrap, but adds a context.Context parameter to every error wrapping function, so that the error can carry its original context as it's returned up the stack.

Every error returned by this package implements the following method:

Context() context.Context

Other libraries (e.g. a logging library) can check for this method, to use the error's original context.

To see why you may want this, let's look at an example using the hermannm.dev/devlog/log logging library:

import (
	"context"

	"hermannm.dev/devlog/log"
	"hermannm.dev/wrap"
)

func parentFunction(ctx context.Context) {
	if err := childFunction(ctx); err != nil {
		log.Error(ctx, err, "Child function failed")
	}
}

func childFunction(ctx context.Context) error {
	// log.AddContextAttrs adds log attributes to the context.
	// When ctx is logged, these attributes are included in the log output
	ctx = log.AddContextAttrs(ctx, "key", "value")

	if err := someFallibleOperation(ctx); err != nil {
		return wrap.Error(err, "Operation failed")
	}

	return nil
}

In the above example, childFunction returns an error, and parentFunction logs it. This is a typical pattern, as errors are often returned up the stack before being logged.

We see that we attach log attributes to the context in childFunction, using log.AddContextAttrs. But when we return the error using wrap.Error, we lose those context attributes! This is not ideal, as we want as much context as possible when an error is logged.

ctxwrap solves this by letting us attach the context to the error, so that the logging library can get the context attributes from the error when it is logged. This revised example uses ctxwrap instead of wrap, to propagate context attributes:

import (
	"context"

	"hermannm.dev/devlog/log"
	"hermannm.dev/wrap/ctxwrap"
)

func parentFunction(ctx context.Context) {
	if err := childFunction(ctx); err != nil {
		log.Error(ctx, err, "Child function failed")
	}
}

func childFunction(ctx context.Context) error {
	ctx = log.AddContextAttrs(ctx, "key", "value")

	if err := someFallibleOperation(ctx); err != nil {
		// Uses ctxwrap to attach the context to the error
		return ctxwrap.Error(ctx, err, "Operation failed")
	}

	return nil
}

Now, when parentFunction logs the error from childFunction, the context attributes carried by the error will be logged, so we get more context in our error log!

Developer's guide

When publishing a new release:

  • Run tests:
    go test ./...
    
  • Add an entry to CHANGELOG.md (with the current date)
    • Remember to update the link section, and bump the version for the [Unreleased] link
  • Create commit and tag for the release (update TAG variable in below command):
    TAG=vX.Y.Z && git commit -m "Release ${TAG}" && git tag -a "${TAG}" -m "Release ${TAG}" && git log --oneline -2
    
  • Push the commit and tag:
    git push && git push --tags
    
    • Our release workflow will then create a GitHub release with the pushed tag's changelog entry

Documentation

Overview

Package wrap provides utility functions to wrap errors with extra context.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Error

func Error(wrapped error, message string) error

Error wraps the given error with a message, to add context to the error.

If you're in a function with a context.Context parameter, consider using hermannm.dev/wrap/ctxwrap.Error instead. See the hermannm.dev/wrap/ctxwrap package docs for why you may want to do this.

The returned error implements the Unwrap method from the standard errors package, so it works with errors.Is and errors.As.

Error string format

The following example:

err := errors.New("duplicate primary key")
wrapped := wrap.Error(err, "database insert failed")
fmt.Println(wrapped)

...produces this error string:

database insert failed
- duplicate primary key

Wrapped errors can be nested. Wrapping an already wrapped error adds it to the error list, so this next example:

err := errors.New("duplicate primary key")
inner := wrap.Error(err, "database insert failed")
outer := wrap.Error(inner, "failed to store event")
fmt.Println(outer)

...produces this error string:

failed to store event
- database insert failed
- duplicate primary key

func ErrorWithAttrs added in v0.4.0

func ErrorWithAttrs(wrapped error, message string, logAttributes ...any) error

ErrorWithAttrs wraps the given error with a message and log attributes, to add structured context to the error when it is logged (see below for how to pass attributes).

The returned error implements the following method:

LogAttrs() []slog.Attr

A logging library can check for the existence of this method when an error is logged, to add these attributes to the log output. The hermannm.dev/devlog/log library, which wraps log/slog, does this in its error-aware logging functions.

If you're in a function with a context.Context parameter, consider using hermannm.dev/wrap/ctxwrap.ErrorWithAttrs instead. See the hermannm.dev/wrap/ctxwrap package docs for why you may want to do this.

The returned error also implements the Unwrap method from the standard errors package, so it works with errors.Is and errors.As.

Log attributes

A log attribute (abbreviated "attr") is a key-value pair attached to a log line. You can pass attributes in the following ways:

// Pairs of string keys and corresponding values:
wrap.ErrorWithAttrs(err, "error message", "key1", "value1", "key2", 2)
// slog.Attr objects:
wrap.ErrorWithAttrs(err, "error message", slog.String("key1", "value1"), slog.Int("key2", 2))
// Or a mix of the two:
wrap.ErrorWithAttrs(err, "error message", "key1", "value1", slog.Int("key2", 2))

When outputting logs as JSON (using e.g. slog.JSONHandler), these become fields in the logged JSON object. This allows you to filter and query on the attributes in the log analysis tool of your choice, in a more structured manner than if you were to just use string concatenation.

Error string format

The following example:

err := errors.New("duplicate primary key")
wrapped := wrap.Error(err, "database insert failed")
fmt.Println(wrapped)

...produces this error string:

database insert failed
- duplicate primary key

Wrapped errors can be nested. Wrapping an already wrapped error adds it to the error list, so this next example:

err := errors.New("duplicate primary key")
inner := wrap.Error(err, "database insert failed")
outer := wrap.Error(inner, "failed to store event")
fmt.Println(outer)

...produces this error string:

failed to store event
- database insert failed
- duplicate primary key

func Errorf

func Errorf(wrapped error, messageFormat string, formatArgs ...any) error

Errorf wraps the given error with a formatted message, to add context to the error. It forwards the given message format and args to fmt.Sprintf to construct the message.

If you're in a function with a context.Context parameter, consider using hermannm.dev/wrap/ctxwrap.Errorf instead. See the hermannm.dev/wrap/ctxwrap package docs for why you may want to do this.

The returned error implements the Unwrap method from the standard errors package, so it works with errors.Is and errors.As.

Error string format

The following example:

err := errors.New("unrecognized event type")
wrapped := wrap.Errorf(err, "failed to process event of type '%s'", "ORDER_UPDATED")
fmt.Println(wrapped)

...produces this error string:

failed to process event of type 'ORDER_UPDATED'
- unrecognized event type

func Errors

func Errors(wrapped []error, message string) error

Errors wraps the given errors with a message, to add context to the errors.

If you're in a function with a context.Context parameter, consider using hermannm.dev/wrap/ctxwrap.Errors instead. See the hermannm.dev/wrap/ctxwrap package docs for why you may want to do this.

The returned error implements the Unwrap method from the standard errors package, so it works with errors.Is and errors.As.

Error string format

The following example:

errs := []error{errors.New("invalid timestamp format"), errors.New("id was not UUID")}
wrapped := wrap.Errors(errs, "failed to parse event")
fmt.Println(wrapped)

...produces this error string:

failed to parse event
- invalid timestamp format
- id was not UUID

When combined with wrap.Error, nested wrapped errors are indented, so this next example:

errs := []error{errors.New("invalid timestamp format"), errors.New("id was not UUID")}
inner := wrap.Errors(errs, "failed to parse event")
outer := wrap.Error(inner, "event processing failed")
fmt.Println(outer)

...produces this error string:

event processing failed
- failed to parse event
  - invalid timestamp format
  - id was not UUID

func ErrorsWithAttrs added in v0.4.0

func ErrorsWithAttrs(wrapped []error, message string, logAttributes ...any) error

ErrorsWithAttrs wraps the given errors with a message and log attributes, to add structured context to the error when it is logged (see below for how to pass attributes).

The returned error implements the following method:

LogAttrs() []slog.Attr

A logging library can check for the existence of this method when an error is logged, to add these attributes to the log output. The hermannm.dev/devlog/log library, which wraps log/slog, does this in its error-aware logging functions.

If you're in a function with a context.Context parameter, consider using hermannm.dev/wrap/ctxwrap.ErrorsWithAttrs instead. See the hermannm.dev/wrap/ctxwrap package docs for why you may want to do this.

The returned error also implements the Unwrap method from the standard errors package, so it works with errors.Is and errors.As.

Log attributes

A log attribute (abbreviated "attr") is a key-value pair attached to a log line. You can pass attributes in the following ways:

// Pairs of string keys and corresponding values:
wrap.ErrorsWithAttrs(errs, "error message", "key1", "value1", "key2", 2)
// slog.Attr objects:
wrap.ErrorsWithAttrs(errs, "error message", slog.String("key1", "value1"), slog.Int("key2", 2))
// Or a mix of the two:
wrap.ErrorsWithAttrs(errs, "error message", "key1", "value1", slog.Int("key2", 2))

When outputting logs as JSON (using e.g. slog.JSONHandler), these become fields in the logged JSON object. This allows you to filter and query on the attributes in the log analysis tool of your choice, in a more structured manner than if you were to just use string concatenation.

Error string format

The following example:

errs := []error{errors.New("invalid timestamp format"), errors.New("id was not UUID")}
wrapped := wrap.Errors(errs, "failed to parse event")
fmt.Println(wrapped)

...produces this error string:

failed to parse event
- invalid timestamp format
- id was not UUID

When combined with wrap.Error, nested wrapped errors are indented, so this next example:

errs := []error{errors.New("invalid timestamp format"), errors.New("id was not UUID")}
inner := wrap.Errors(errs, "failed to parse event")
outer := wrap.Error(inner, "event processing failed")
fmt.Println(outer)

...produces this error string:

event processing failed
- failed to parse event
  - invalid timestamp format
  - id was not UUID

func Errorsf added in v0.4.0

func Errorsf(wrapped []error, messageFormat string, formatArgs ...any) error

Errorsf wraps the given errors with a formatted message, to add context to the error. It forwards the given message format and args to fmt.Sprintf to construct the message.

If you're in a function with a context.Context parameter, consider using hermannm.dev/wrap/ctxwrap.Errorsf instead. See the hermannm.dev/wrap/ctxwrap package docs for why you may want to do this.

The returned error implements the Unwrap method from the standard errors package, so it works with errors.Is and errors.As.

Error string format

The following example:

errs := []error{errors.New("invalid timestamp format"), errors.New("id was not UUID")}
wrapped := wrap.Errorsf(errs, "failed to process event of type '%s'", "ORDER_UPDATED")
fmt.Println(wrapped)

...produces this error string:

failed to process event of type 'ORDER_UPDATED'
- invalid timestamp format
- id was not UUID

func NewErrorWithAttrs added in v0.4.0

func NewErrorWithAttrs(message string, logAttributes ...any) error

NewErrorWithAttrs returns a new error with the given message, and logging attributes to add structured context to the error when it is logged (see below for how to pass attributes).

The returned error implements the following method:

LogAttrs() []slog.Attr

A logging library can check for the existence of this method when an error is logged, to add these attributes to the log output. The hermannm.dev/devlog/log library, which wraps log/slog, does this in its error-aware logging functions.

If you're in a function with a context.Context parameter, consider using hermannm.dev/wrap/ctxwrap.NewErrorWithAttrs instead. See the hermannm.dev/wrap/ctxwrap package docs for why you may want to do this.

Log attributes

A log attribute (abbreviated "attr") is a key-value pair attached to a log line. You can pass attributes in the following ways:

// Pairs of string keys and corresponding values:
wrap.NewErrorWithAttrs("error message", "key1", "value1", "key2", 2)
// slog.Attr objects:
wrap.NewErrorWithAttrs("error message", slog.String("key1", "value1"), slog.Int("key2", 2))
// Or a mix of the two:
wrap.NewErrorWithAttrs("error message", "key1", "value1", slog.Int("key2", 2))

When outputting logs as JSON (using e.g. slog.JSONHandler), these become fields in the logged JSON object. This allows you to filter and query on the attributes in the log analysis tool of your choice, in a more structured manner than if you were to just use string concatenation.

Types

This section is empty.

Directories

Path Synopsis
Package ctxwrap tries to solve the problem of errors escaping their context.
Package ctxwrap tries to solve the problem of errors escaping their context.

Jump to

Keyboard shortcuts

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