errors

package module
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Sep 6, 2024 License: Apache-2.0 Imports: 14 Imported by: 66

README

Errors with a stack trace

pkg.go.dev Go Report Card pipeline status coverage report

A Go package providing errors with a stack trace and optional structured details.

Features:

  • Based of github.com/pkg/errors with compatible API, addressing many its open issues. In can be used as a drop-in replacement and even mixed with github.com/pkg/errors.
  • Uses standard error wrapping (available since Go 1.13) and wrapping of multiple errors (available since Go 1.20).
  • All errors expose information through simple interfaces which do not use custom types. This makes errors work without a dependency on this package. They work even across versions of this package.
  • It is interoperable with other popular errors packages and can be used in projects where they are mixed to unify their stack traces and formatting.
  • Provides errors.Errorf which supports %w format verb to both wrap and record a stack trace at the same time (if not already recorded).
  • Provides errors.E type to be used instead of standard error to annotate which functions return errors with a stack trace and details.
  • Clearly defines what are differences and expected use cases for:
    • errors.Errorf: creating a new error and recording a stack trace, optionally wrapping an existing error
    • errors.WithStack: adding a stack trace to an error without one
    • errors.WithMessage: adding a prefix to the error message
    • errors.Wrap and errors.WrapWith: creating a new error but recording its cause
    • errors.Join: joining multiple errors which happened during execution (e.g., additional errors which happened during cleanup)
    • errors.Prefix: combine multiple errors by prefixing an error with base errors
  • Provides errors.Base function to create errors without a stack trace to be used as base errors for errors.Is and errors.As.
  • Differentiates between wrapping and recording a cause: errors.Wrap and errors.WrapWith record a cause, while other functions are error transformers, wrapping the original.
  • Makes sure a stack trace is not recorded multiple times unnecessarily.
  • Provides optional details map on all errors returned by this package. Helper errors.WithDetails allows both recording a stack trace and annotating an error with details at the same time.
  • Errors and stack traces support configurable formatting and can be marshaled into JSON. Both formatting and JSON marshaling is supported also for errors not made using this package.
  • Limited JSON unmarshal of errors is supported to enable formatting of JSON errors.

Installation

This is a Go package. You can add it to your project using go get:

go get gitlab.com/tozd/go/errors

It requires Go 1.17 or newer.

Usage

See full package documentation with examples on pkg.go.dev.

Why a new Go errors package?

github.com/pkg/errors package is archived and not developed anymore, with many issues not addressed (primarily because many require some backward incompatible change). At the same time it has been made before Go 1.13 added official support for wrapping errors and it does not (and cannot, in backwards compatible way) fully embrace it. This package takes what is best from github.com/pkg/errors, but breaks things a bit to address many of the open issues community has identified since then and to modernize it to today's Go:

  • Message formatting WithMessage vs. Wrap: #114
  • Do not re-add stack trace if one is already there: #122
  • Be explicit when you want to record a stack trace again vs. do not if it already exists: #75 #158 #242
  • StackTrace() should return []uintptr: #79
  • Do not assume Cause cannot return nil: #89
  • Obtaining only message from Wrap: #93
  • WithMessage always prefixes the message: #102
  • Differentiate between "wrapping" and "causing": #112
  • Support for base errors: #130 #160
  • Support for a different delimiter by supporting Errorf: #207 #226
  • Support for Errorf wrapping an error: #244
  • Having each function wrap only once: #223

What are main differences from github.com/pkg/errors?

In most cases this package can be used as a drop-in replacement for github.com/pkg/errors, but there are some small (behavioral) differences (i.e., improvements):

  • The stackTracer interface's StackTrace() method returns []uintptr and not custom type StackTrace.
  • All error-wrapping functions return errors which implement the standard unwrapper interface, but only errors.Wrap records a cause error and returns an error which implements the causer interface.
  • All error-wrapping functions wrap the error into only one new error.
  • Only errors.Wrap always records the stack trace while other functions do not record if it is already present.
  • errors.Cause repeatedly unwraps the error until it finds one which implements the causer interface, and then return its cause.

Main additions are:

  • Errorf supports %w.
  • This package supports annotating errors with additional key-value details.
  • This package provides more configurable formatting and JSON marshaling of stack traces and errors.
  • Support for base errors (e.g., errors.Base and errors.WrapWith) instead of just operating on error messages.

How to migrate from github.com/pkg/errors?

  1. Replace your github.com/pkg/errors imports with gitlab.com/tozd/go/errors imports.
  2. Run go get gitlab.com/tozd/go/errors.

In most cases, this should be it. Consider:

  • Migrating to using errors.Errorf (which supports %w) as it subsumes most of github.com/pkg/errors's error constructors.
  • Using errors.E return type instead of plain error. This makes Go type system help you to not return an error without a stack trace.
  • Using structured details with errors.WithDetails.
  • Using base errors instead of message-based error constructors, especially if you want callers of your functions to handle different errors differently.

How to migrate from standard errors and fmt.Errorf?

  1. Replace your errors imports with gitlab.com/tozd/go/errors imports.
  2. Replace your fmt.Errorf calls with errors.Errorf calls.
  3. If you have top-level errors.New or fmt.Errorf/errors.Errorf calls to create variables with base errors, use errors.Base and errors.Basef instead. Otherwise your stack traces will not be recorded correctly.

This is it. Now all your errors automatically record stack traces. You can now print those stack traces when formatting errors using fmt.Printf("%+v", err) or marshal them to JSON.

Consider:

  • Using structured details with errors.WithDetails.
  • Using errors.E return type instead of plain error. This makes Go type system help you to not return an error without a stack trace.
  • Using base errors instead of message-based error constructors, especially if you want callers of your functions to handle different errors differently.

This package provides everything errors package does (but with stack traces and optional details) in a compatible way, often simply proxying calls to the standard errors package, but there is a difference how errors.Join operates: if only one non-nil error is provided, errors.Join returns it as-is without wrapping it. Furthermore, errors.Join records a stack trace at the point it was called.

How should this package be used?

Patterns for errors in Go have evolved through time, with Go 1.13 introducing error wrapping (and Go 1.20 wrapping of multiple errors) enabling standard annotation of errors with additional information. For example, standard fmt.Errorf allows annotating the error message of a base error with additional information, e.g.:

import (
  "errors"
  "fmt"
)

var ErrMy = errors.New("my error")

// later on

err := fmt.Errorf(`user "%s" made an error: %w`, username, ErrMy)

This is great because one can later on extract the cause of the error using errors.Is without having to parse the error message itself:

if errors.Is(err, ErrMy) {
  ...
}

But if ErrMy can happen at multiple places it is hard to debug without additional information where this error happened. One can add it to the error message, but this is tedious:

err := fmt.Errorf(`user "%s" made an error during sign-in: %w`, username, ErrMy)

// somewhere else

err := fmt.Errorf(`user "%s" made an error during sign-out: %w`, username, ErrMy)

Furthermore, if you need to extract the username you again have to parse the error message.

Instead, this package provides support for recording a stack trace optional structured details:

import (
  "gitlab.com/tozd/go/errors"
)

var ErrMy = errors.Base("my error")
var ErrUser = errors.Basef("user made an error: %w", ErrMy)

// later on

err := errors.WithDetails(ErrUser, "username", username)

Here, err contains a descriptive error message (without potentially sensitive information), a stack trace, and easy to extract username. You can display all that to the developer using fmt.Printf:

fmt.Printf("error: %#+v", err)

Extracting username is easy:

errors.AllDetails(err)["username"]

And if you need to know exactly which error happened (e.g., to show a translated error message to the end user), you can use errors.Is or similar logic (to map between errors and their user-friendly translated error messages).

The package provides errors.Errorf, errors.Wrap and other message-based error constructors, but they are provided primarily for compatibility (and to support patterns different from the suggested one below).

Suggestion
  • Use errors.Base, errors.Basef, errors.BaseWrap and errors.BaseWrapf to create a tree of constant base errors.
  • Do not use errors.Errorf but use errors.WithDetails with a base error to record both a stack trace and any additional structured details at point where the error occurs.
  • You can use errors.WithStack or errors.WithDetails to do the same with errors coming from outside of your codebase, as soon as possible. If it is useful to know at a glance where the error is coming from, consider using errors.WithMessage to prefix their error messages with the source of the error (e.g, external function name).
  • If you want to map one error to another while recording the cause, use errors.WrapWith. If you want to reuse the error message, use errors.Prefix or errors.Errorf (but only to control how messages are combined).
  • If errors coming from outside of your codebase do not provide adequate base errors and base errors are needed, use errors.WrapWith, errors.Prefix, or errors.Errorf (but only to control how messages are combined) to provide them yourself.
  • If during handling of an error another error occurs (e.g., in defer during cleanup) use errors.Join or errors.Errorf (but only to control how messages are joined) to join them all.
  • Use errors.E return type instead of plain error. This makes Go type system help you to not return an error without a stack trace.

Your functions should return only errors for which you provide base errors as well. Those base errors become part of your API. All additional (non-constant) information about the particular error goes into its stack trace and details.

Do not overdo the approach with base errors, e.g., do not make them too granular. Design them as you design your API and consider their use cases. Remember, errors also have stack traces to help you understand where are they coming from. This holds also for prefixing error messages, prefix them only if it makes error messages clearer and not to make it into something resembling a call trace.

Remember, error messages, stack traces, and details are for developers not end users. Be mindful if and how you expose them to end users.

I use base errors, but stack traces do not look right?

Let's assume you use errors.New to create a base error:

var ErrMyBase = errors.New("error")

Later on, you want to annotate it with a stack trace and return it from the function where the error occurred:

func run() error.E {
  // ... do something ...
  return errors.WithStack(ErrMyBase)
}

Sadly, this does not work as expected. If you use errors.New (or errors.Errorf) to create a base error, a stack trace is recorded at the point it was called. errors.WithStack then does nothing because it detects that a stack trace already exists. So the stack trace you get is from the point you created the base error.

You should create base errors using errors.Base, errors.Basef, errors.BaseWrap and errors.BaseWrapf. They create errors without stack traces. And errors.WithStack adds a stack trace at the point it was called.

It looks like Wrap should be named Cause or WithCause. Why it is not?

For legacy reasons because this package builds on shoulders of github.com/pkg/errors. Every modification to errors made through this package is done through wrapping so that original error is always available. Wrap wraps the error to records the cause. Cause exist as a helper to return the recorded cause.

Is this package a fork of github.com/pkg/errors?

No. This is a completely new package which has been written from scratch using current best practices and patterns in Go and Go errors. But one of its goals is to be (in most cases) a drop-in replacement for github.com/pkg/errors so it shares API with github.com/pkg/errors while providing at the same time new utility functions and new functionality.

It looks like most of the things provided by this package can be done using standard errors package?

That is the idea! One of the goals of this package is to learn from github.com/pkg/errors and update it to modern Go errors patterns. That means that it is interoperable with other errors packages and can be used in large codebases with mixed use of different errors packages. The idea is that you should be able to create errors which behave like errors from this package by implementing few interfaces and this package knows how to use other errors if they implement those interfaces, too.

What this package primarily provides are utility functions for common cases so that it is just easier to do "the right thing" and construct useful errors. It even calls into standard errors package itself for most of the heavy lifting. Despite many features this package provides this approach keeps it pretty lean.

This package uses github.com/pkg/errors under the hood!

No, it does not. It is a completely new package. But because it wants to be compatible with errors made by github.com/pkg/errors in large codebases with mixed use of different errors packages it has to depend on github.com/pkg/errors to get one type (errors.StackTrace) from it so that it can extract stack traces from those errors. It is using no code from github.com/pkg/errors.

BTW, this package itself does not require to import it to be able to extract all data from its errors. Interfaces used by this package do not use custom types. Another lesson learned from github.com/pkg/errors.

  • github.com/cockroachdb/errors – Go errors with every possible feature you might ever need in your large project. Internally it uses deprecated github.com/pkg/errors. This package aims to stay lean and be more or less just a drop-in replacement for core Go errors and archived github.com/pkg/errors, but with stack traces and structured details (and few utility functions for common cases).
  • github.com/friendsofgo/errors – A fork of github.com/pkg/errors but beyond updating the code to error wrapping introduced in Go 1.13 it does not seem maintained.
  • github.com/go-errors/errors – Another small error package with stack trace recording support, but with different API than github.com/pkg/errors. It does not support structured details, extended formatting nor JSON marshal.
  • github.com/rotisserie/eris – Eris has some similar features, like recording a stack trace and support for JSON. It supports additional features, like more formatting options and built-in Sentry support. It can be used as a replacement for github.com/pkg/errors but some functions are missing (e.g., WithMessage, WithStack) while this package provides them. Eris also does not support structured details.
  • github.com/ztrue/tracerr – A simple library to create errors with a stack trace. It is able to also show snippets of source code when formatting stack traces. But it is slower than this package because it resolves stack traces at error creation time and not only when formatting. It also lacks many github.com/pkg/errors functions so it does not work as a drop-in replacement.
  • emperror.dev/errors – A drop-in replacement for github.com/pkg/errors with similar features to this package, embracing modern Go errors patterns (wrapping, sentinel/base errors, etc.) and sharing similar design goals. It supports structured details as well. It has different integrations with other packages and services. But under the hood there are small but important differences: this package supports %w in errors.Errorf, it does not use github.com/pkg/errors internally, you can obtain a stack trace from errors without importing this package, it supports more formatting options and JSON marshaling of errors.
  • github.com/axkit/errors – Supports recording a stack trace, structured details, severity levels, JSON marshaling, etc. But it inefficiently resolves stack traces at error creation time and not only when formatting. It requires importing the package to access the stack trace. Its API is different from github.com/pkg/errors.
  • github.com/efficientgo/core/errors – A minimal library for wrapping errors with stack traces. Does not provide errors.Errorf not does it support %w format verb. It is not possible to access unformatted stack trace.

GitHub mirror

There is also a read-only GitHub mirror available, if you need to fork the project there.

Documentation

Overview

Package errors provides errors with a recorded stack trace and optional structured details.

The traditional error handling idiom in Go is roughly akin to

if err != nil {
        return err
}

which when applied recursively up the call stack results in error reports without a stack trace or context. The errors package provides error handling primitives to annotate errors along the failure path in a way that does not destroy the original error.

Adding a stack trace to an error

When interacting with code which returns errors without a stack trace, you can upgrade that error to one with a stack trace using errors.WithStack. For example:

func readAll(r io.Reader) ([]byte, errors.E) {
        data, err := ioutil.ReadAll(r)
        if err != nil {
                return nil, errors.WithStack(err)
        }
        return data, nil
}

errors.WithStack records the stack trace at the point where it was called, so use it as close to where the error originated as you can get so that the recorded stack trace is more precise.

The example above uses errors.E for the returned error type instead of the standard error type. This is not required, but it tells Go that you expect that the function returns only errors with a stack trace and Go type system then helps you find any cases where this is not so.

errors.WithStack does not record the stack trace if it is already present in the error so it is safe to call it if you are unsure if the error contains a stack trace.

Errors with a stack trace implement the following interface, returning program counters of function invocations:

type stackTracer interface {
        StackTrace() []uintptr
}

You can use standard runtime.CallersFrames to obtain stack trace frame information (e.g., function name, source code file and line). You can also use errors.StackFormatter to format the stack trace.

Although the stackTracer interface is not exported by this package, it is considered a part of its stable public interface.

Adding context to an error

Sometimes an error occurs in a low-level function and the error messages returned from it are too low-level, too. You can use errors.Wrap to construct a new higher-level error while recording the original error as a cause.

image, err := readAll(imageFile)
if err != nil {
        return nil, errors.Wrap(err, "reading image failed")
}

In the example above we returned a new error with a new message, hiding the low-level details. The returned error implements the following interface:

type causer interface {
        Cause() error
}

which enables access to the underlying low-level error. You can also use errors.Cause to obtain the cause.

Although the causer interface is not exported by this package, it is considered a part of its stable public interface.

Sometimes you do not want to hide the error message but just add to it. You can use errors.WithMessage, which adds a prefix to the existing message, or errors.Errorf, which gives you more control over the new message.

errors.WithMessage(err, "reading image failed")
errors.Errorf("reading image failed (%w)", err)

Example new messages could then be, respectively:

"reading image failed: connection error"
"reading image failed (connection error)"

Adding details to an error

Errors returned by this package implement the detailer interface:

type detailer interface {
        Details() map[string]interface{}
}

which enables access to a map with optional additional details about the error. Returned map can be modified in-place. You can also use errors.Details and errors.AllDetails to access details:

errors.Details(err)["url"] = "http://example.com"

You can also use errors.WithDetails as an alternative to errors.WithStack if you also want to add details while recording the stack trace:

func readAll(r io.Reader, filename string) ([]byte, errors.E) {
        data, err := ioutil.ReadAll(r)
        if err != nil {
                return nil, errors.WithDetails(err, "filename", filename)
        }
        return data, nil
}

Working with the tree of errors

Errors which implement the following standard unwrapper interfaces:

type unwrapper interface {
        Unwrap() error
}

type unwrapper interface {
        Unwrap() error[]
}

form a tree of errors where a wrapping error points its parent, wrapped, error(s). Errors returned from this package implement this interface to return the original error or errors, when they exist. This enables us to have constant base errors which we annotate with a stack trace before we return them:

var ErrAuthentication = errors.Base("authentication error")
var ErrMissingPassphrase = errors.BaseWrap(ErrAuthentication, "missing passphrase")
var ErrInvalidPassphrase = errors.BaseWrap(ErrAuthentication, "invalid passphrase")

func authenticate(passphrase string) errors.E {
        if passphrase == "" {
                return errors.WithStack(ErrMissingPassphrase)
        } else if passphrase != "open sesame" {
                return errors.WithStack(ErrInvalidPassphrase)
        }
        return nil
}

Or with details:

func authenticate(username, passphrase string) errors.E {
        if passphrase == "" {
                return errors.WithDetails(ErrMissingPassphrase, "username", username)
        } else if passphrase != "open sesame" {
                return errors.WithDetails(ErrInvalidPassphrase, "username", username)
        }
        return nil
}

We can use errors.Is to determine which error has been returned:

if errors.Is(err, ErrMissingPassphrase) {
        fmt.Println("Please provide a passphrase to unlock the doors.")
}

Works across the tree, too:

if errors.Is(err, ErrAuthentication) {
        fmt.Println("Failed to unlock the doors.")
}

To access details, use:

errors.AllDetails(err)["username"]

You can join multiple errors into one error by calling errors.Join. Join also records the stack trace at the point it was called.

Formatting and JSON marshaling errors

All errors with a stack trace returned from this package implement fmt.Formatter interface and can be formatted by the fmt package. They also support marshaling to JSON. Same formatting and JSON marshaling for errors coming outside of this package can be done by wrapping them into errors.Formatter.

Example (StackTrace)
package main

import (
	"fmt"
	"runtime"

	"gitlab.com/tozd/go/errors"
)

func getErr() error {
	return errors.New("foobar")
}

func main() {
	type stackTracer interface {
		StackTrace() []uintptr
	}

	var err stackTracer
	if !errors.As(getErr(), &err) {
		panic(errors.New("oops, err does not implement stackTracer"))
	}

	frames := runtime.CallersFrames(err.StackTrace())
	frame, _ := frames.Next()
	fmt.Printf("%s\n\t%s:%d", frame.Function, frame.File, frame.Line)

	// Example output:
	// gitlab.com/tozd/go/errors_test.getErr
	//	/home/user/errors/example_test.go:11
}
Output:

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrUnsupported = stderrors.ErrUnsupported

ErrUnsupported indicates that a requested operation cannot be performed, because it is unsupported. For example, a call to os.Link when using a file system that does not support hard links.

Functions and methods should not return this error but should instead return an error including appropriate context that satisfies

errors.Is(err, errors.ErrUnsupported)

either by directly wrapping ErrUnsupported or by implementing an Is method.

Functions and methods should document the cases in which an error wrapping this will be returned.

This variable is the same as the standard errors.ErrUnsupported.

Functions

func AllDetails added in v0.4.0

func AllDetails(err error) map[string]interface{}

AllDetails returns a map build from calling the Details method on err and populating the map with key/value pairs which are not yet present. Afterwards, the err is unwrapped and the process is repeated. Unwrapping stops if it encounters an error with the Cause method returning error, or Unwrap() method returning multiple errors.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	base := errors.Base("not found")
	err1 := errors.WithDetails(base, "file", "plans.txt")
	err2 := errors.WithDetails(err1, "user", "vader")
	fmt.Println(errors.AllDetails(err1))
	fmt.Println(errors.AllDetails(err2))
}
Output:

map[file:plans.txt]
map[file:plans.txt user:vader]

func As

func As(err error, target interface{}) bool

As finds the first error in err's tree that matches target, and if one is found, sets target to that error value and returns true. Otherwise, it returns false.

The tree consists of err itself, followed by the errors obtained by repeatedly calling Unwrap. When err wraps multiple errors, As examines err followed by a depth-first traversal of its children.

An error matches target if the error's concrete value is assignable to the value pointed to by target, or if the error has a method As(interface{}) bool such that As(target) returns true. In the latter case, the As method is responsible for setting target.

An error type might provide an As method so it can be treated as if it were a different error type.

As panics if target is not a non-nil pointer to either a type that implements error, or to any interface type.

This function is a proxy for standard errors.As.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

type MyError struct {
	code    int
	message string
}

func (e MyError) Error() string {
	return e.message
}

func (e MyError) Code() int {
	return e.code
}

var (
	ErrBadRequest = &MyError{400, "error"}
	ErrNotFound   = &MyError{404, "not found"}
)

func getMyErr() error {
	return ErrNotFound
}

func main() {
	err := getMyErr()

	var myErr *MyError
	if errors.As(err, &myErr) {
		fmt.Printf("code: %d", myErr.Code())
	}
}
Output:

code: 404

func Base

func Base(message string) error

Base returns an error with the supplied message. Each call to Base returns a distinct error value even if the message is identical. It does not record a stack trace.

Use Base for a constant base error you convert to an actual error you return with WithStack or WithDetails. This base error you can then use in Is and As calls.

This function is a proxy for standard errors.New.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	err := errors.Base("whoops")
	fmt.Println(err)
}
Output:

whoops

func BaseWrap

func BaseWrap(err error, message string) error

BaseWrap returns an error with the supplied message, wrapping an existing error err. Each call to BaseWrap returns a distinct error value even if the message is identical. It does not record a stack trace.

Use BaseWrap when you want to create a tree of base errors and you want to fully control the error message.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	base := errors.Base("error")
	valueError := errors.BaseWrap(base, "value")
	fmt.Println(valueError)
}
Output:

value

func BaseWrapf

func BaseWrapf(err error, format string, args ...interface{}) error

BaseWrapf returns an error with the supplied message formatted according to a format specifier. Each call to BaseWrapf returns a distinct error value even if the message is identical. It does not record a stack trace. It does not support %w format verb (use %s instead if you need to incorporate error's error message, but then you can also just use Basef).

Use BaseWrapf when you want to create a tree of base errors and you want to fully control the error message.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	base := errors.Base("error")
	valueError := errors.BaseWrapf(base, "value %d", 2)
	fmt.Println(valueError)
}
Output:

value 2

func Basef

func Basef(format string, args ...interface{}) error

Basef returns an error with the supplied message formatted according to a format specifier. Each call to Basef returns a distinct error value even if the message is identical. It does not record a stack trace. It supports %w format verb to wrap an existing error. %w can be provided multiple times.

Use Basef for a constant base error you convert to an actual error you return with WithStack or WithDetails. This base error you can then use in Is and As calls. Use %w format verb when you want to create a tree of base errors.

This function is a proxy for standard fmt.Errorf.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	err := errors.Basef("whoops #%d", 2)
	fmt.Println(err)
}
Output:

whoops #2

func Cause

func Cause(err error) error

Cause returns the result of calling the Cause method on err, if err's type contains a Cause method returning error. Otherwise, the err is unwrapped and the process is repeated. If unwrapping is not possible, Cause returns nil. Unwrapping stops if it encounters an error with Unwrap() method returning multiple errors.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	base := errors.Base("error")
	wrapped := errors.Wrap(base, "wrapped")
	fmt.Println(errors.Cause(wrapped))
}
Output:

error

func Details added in v0.4.0

func Details(err error) map[string]interface{}

Details returns the result of calling the Details method on err, if err's type contains a Details method returning initialized map. Otherwise, the err is unwrapped and the process is repeated. If unwrapping is not possible, Details returns nil. Unwrapping stops if it encounters an error with the Cause method returning error, or Unwrap() method returning multiple errors.

You can modify returned map to modify err's details.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	base := errors.Base("not found")
	err := errors.WithStack(base)
	errors.Details(err)["file"] = "plans.txt"
	errors.Details(err)["user"] = "vader"
	fmt.Println(errors.Details(err))
}
Output:

map[file:plans.txt user:vader]

func Is

func Is(err, target error) bool

Is reports whether any error in err's tree matches target.

The tree consists of err itself, followed by the errors obtained by repeatedly calling Unwrap. When err wraps multiple errors, Is examines err followed by a depth-first traversal of its children.

An error is considered to match a target if it is equal to that target or if it implements a method Is(error) bool such that Is(target) returns true.

An error type might provide an Is method so it can be treated as equivalent to an existing error. For example, if MyError defines

func (m MyError) Is(target error) bool { return target == fs.ErrExist }

then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for an example in the standard library. An Is method should only shallowly compare err and the target and not call Unwrap on either.

This function is a proxy for standard errors.Is.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	base := errors.Base("error")
	valueError := errors.BaseWrap(base, "value")
	fmt.Println(errors.Is(valueError, base))
}
Output:

true

func Unjoin added in v0.6.0

func Unjoin(err error) []error

Unjoin returns the result of calling the Unwrap method on err, if err's type contains an Unwrap method returning multiple errors. Otherwise, the err is unwrapped and the process is repeated. If unwrapping is not possible, Unjoin returns nil. Unwrapping stops if it encounters an error with the Cause method returning error.

func Unwrap

func Unwrap(err error) error

Unwrap returns the result of calling the Unwrap method on err, if err's type contains an Unwrap method returning error. Otherwise, Unwrap returns nil.

Unwrap only calls a method of the form "Unwrap() error". In particular Unwrap does not unwrap errors returned by Join.

This function is a proxy for standard errors.Unwrap and is not an inverse of errors.Wrap. For that use errors.Cause.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	base := errors.Base("error")
	withPrefix := errors.WithMessage(base, "prefix")
	fmt.Println(withPrefix)
	fmt.Println(errors.Unwrap(withPrefix))
}
Output:

prefix: error
error

Types

type E

type E interface {
	error
	// contains filtered or unexported methods
}

E interface can be used in as a return type instead of the standard error interface to annotate which functions return an error with a stack trace and details. This is useful so that you know when you should use WithStack or WithDetails (for functions which do not return E) and when not (for functions which do return E).

If you call WithStack on an error with a stack trace nothing bad happens (same error is simply returned), it just pollutes the code. So this interface is defined to help. (Calling WithDetails on an error with details adds an additional and independent layer of details on top of any existing details.)

func Errorf

func Errorf(format string, args ...interface{}) E

Errorf return an error with the supplied message formatted according to a format specifier. It supports %w format verb to wrap an existing error. Errorf also records the stack trace at the point it was called, unless wrapped error already have a stack trace. If %w is provided multiple times, then a stack trace is always recorded.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	err := errors.Errorf("whoops: %s", "foo")
	fmt.Printf("%+-v", err)

	// Example output:
	// whoops: foo
	// stack trace (most recent call first):
	// gitlab.com/tozd/go/errors_test.ExampleErrorf
	// 	/home/user/errors/example_test.go:165
	// testing.runExample
	// 	/usr/local/go/src/testing/run_example.go:63
	// testing.runExamples
	// 	/usr/local/go/src/testing/example.go:44
	// testing.(*M).Run
	// 	/usr/local/go/src/testing/testing.go:1927
	// main.main
	// 	_testmain.go:131
	// runtime.main
	// 	/usr/local/go/src/runtime/proc.go:267
	// runtime.goexit
	// 	/usr/local/go/src/runtime/asm_amd64.s:1650
}
Output:

Example (Wrap)
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	base := errors.Base("whoops")
	err := errors.Errorf("oh noes (%w)", base)
	fmt.Printf("%+-v", err)

	// Example output:
	// oh noes (whoops)
	// stack trace (most recent call first):
	// gitlab.com/tozd/go/errors_test.ExampleErrorf_wrap
	// 	/home/user/errors/example_test.go:189
	// testing.runExample
	// 	/usr/local/go/src/testing/run_example.go:63
	// testing.runExamples
	// 	/usr/local/go/src/testing/example.go:44
	// testing.(*M).Run
	// 	/usr/local/go/src/testing/testing.go:1927
	// main.main
	// 	_testmain.go:131
	// runtime.main
	// 	/usr/local/go/src/runtime/proc.go:267
	// runtime.goexit
	// 	/usr/local/go/src/runtime/asm_amd64.s:1650
}
Output:

func Join added in v0.5.0

func Join(errs ...error) E

Join returns an error that wraps the given errors. Join also records the stack trace at the point it was called. Any nil error values are discarded. Join returns nil if errs contains no non-nil values. If there is only one non-nil value, Join behaves like WithStack on the non-nil value. The error formats as the concatenation of the strings obtained by calling the Error method of each element of errs, with a newline between each string.

Join is similar to Errorf("%w\n%w\n", err1, err2), but supports dynamic number of errors, skips nil errors, and it returns the error as-is if there is only one non-nil error already with a stack trace.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	err1 := errors.New("error1")
	err2 := errors.New("error2")
	err := errors.Join(err1, err2)
	fmt.Printf("% +-.1v", err)

	// Example output:
	// error1
	// error2
	// stack trace (most recent call first):
	// gitlab.com/tozd/go/errors_test.ExampleJoin
	// 	/home/user/errors/example_test.go:265
	// testing.runExample
	// 	/usr/local/go/src/testing/run_example.go:63
	// testing.runExamples
	// 	/usr/local/go/src/testing/example.go:44
	// testing.(*M).Run
	// 	/usr/local/go/src/testing/testing.go:1927
	// main.main
	// 	_testmain.go:131
	// runtime.main
	// 	/usr/local/go/src/runtime/proc.go:267
	// runtime.goexit
	// 	/usr/local/go/src/runtime/asm_amd64.s:1650
	//
	// the above error joins errors:
	//
	// 	error1
	// 	stack trace (most recent call first):
	// 	gitlab.com/tozd/go/errors_test.ExampleJoin
	// 		/home/user/errors/example_test.go:263
	// 	testing.runExample
	// 		/usr/local/go/src/testing/run_example.go:63
	// 	testing.runExamples
	// 		/usr/local/go/src/testing/example.go:44
	// 	testing.(*M).Run
	// 		/usr/local/go/src/testing/testing.go:1927
	// 	main.main
	// 		_testmain.go:131
	// 	runtime.main
	// 		/usr/local/go/src/runtime/proc.go:267
	// 	runtime.goexit
	// 		/usr/local/go/src/runtime/asm_amd64.s:1650
	//
	// 	error2
	// 	stack trace (most recent call first):
	// 	gitlab.com/tozd/go/errors_test.ExampleJoin
	// 		/home/user/errors/example_test.go:264
	// 	testing.runExample
	// 		/usr/local/go/src/testing/run_example.go:63
	// 	testing.runExamples
	// 		/usr/local/go/src/testing/example.go:44
	// 	testing.(*M).Run
	// 		/usr/local/go/src/testing/testing.go:1927
	// 	main.main
	// 		_testmain.go:131
	// 	runtime.main
	// 		/usr/local/go/src/runtime/proc.go:267
	// 	runtime.goexit
	// 		/usr/local/go/src/runtime/asm_amd64.s:1650
}
Output:

Example (Defer)
package main

import (
	"fmt"
	"os"

	"gitlab.com/tozd/go/errors"
)

func run() (errE errors.E) {
	file, err := os.CreateTemp("", "test")
	if err != nil {
		return errors.WithStack(err)
	}
	defer func() {
		errE = errors.Join(errE, errors.WithStack(os.Remove(file.Name())))
	}()

	// Do something with the file...

	return nil
}

func main() {
	errE := run()
	if errE != nil {
		fmt.Printf("error: %+v\n", errE)
	} else {
		fmt.Printf("success\n")
	}
}
Output:

success

func New

func New(message string) E

New returns an error with the supplied message. New also records the stack trace at the point it was called.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	err := errors.New("whoops")
	fmt.Println(err)
}
Output:

whoops
Example (Printf)
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	err := errors.New("whoops")
	fmt.Printf("%+-v", err)

	// Example output:
	// whoops
	// stack trace (most recent call first):
	// gitlab.com/tozd/go/errors_test.ExampleNew_printf
	// 	/home/user/errors/example_test.go:16
	// testing.runExample
	// 	/usr/local/go/src/testing/run_example.go:63
	// testing.runExamples
	// 	/usr/local/go/src/testing/example.go:44
	// testing.(*M).Run
	// 	/usr/local/go/src/testing/testing.go:1927
	// main.main
	// 	_testmain.go:131
	// runtime.main
	// 	/usr/local/go/src/runtime/proc.go:267
	// runtime.goexit
	// 	/usr/local/go/src/runtime/asm_amd64.s:1650
}
Output:

func Prefix added in v0.7.0

func Prefix(err error, prefix ...error) E

Prefix annotates err with a prefix message or messages of prefix errors, wrapping prefix errors at the same time. This is similar to WithMessage but instead of using just an error message, you can provide a base error instead. If err does not have a stack trace, stack strace is recorded as well.

It does not support controlling the delimiter. Use Errorf if you need that.

If err is nil, Prefix returns nil.

Use Prefix when you want to make a new error using a base error or base errors and want to construct the new message through common prefixing. If you want to control how are messages combined, use Errorf. If you want to fully replace the message, use WrapWith.

Prefix is similar to Errorf("%w: %w", prefixErr, err), but supports dynamic number of prefix errors, skips nil errors, does not record a stack trace if err already has it, and it returns nil if err is nil.

Example
package main

import (
	"fmt"
	"io"

	"gitlab.com/tozd/go/errors"
)

func main() {
	err := io.EOF
	baseErr := errors.Base("an error")

	errE := errors.Prefix(err, baseErr)
	fmt.Println(errE)
}
Output:

an error: EOF
Example (Printf)
package main

import (
	"fmt"
	"io"

	"gitlab.com/tozd/go/errors"
)

func main() {
	err := io.EOF
	baseErr := errors.Base("an error")

	errE := errors.Prefix(err, baseErr)
	fmt.Printf("% +-.1v", errE)

	// Example output:
	// an error: EOF
	// stack trace (most recent call first):
	// gitlab.com/tozd/go/errors_test.ExamplePrefix_printf
	// 	/home/user/errors/example_test.go:575
	// testing.runExample
	// 	/usr/local/go/src/testing/run_example.go:63
	// testing.runExamples
	// 	/usr/local/go/src/testing/example.go:44
	// testing.(*M).Run
	// 	/usr/local/go/src/testing/testing.go:1927
	// main.main
	// 	_testmain.go:163
	// runtime.main
	// 	/usr/local/go/src/runtime/proc.go:267
	// runtime.goexit
	// 	/usr/local/go/src/runtime/asm_amd64.s:1650
	//
	// the above error joins errors:
	//
	// 	an error
	//
	// 	EOF
}
Output:

func UnmarshalJSON added in v0.6.0

func UnmarshalJSON(data []byte) (error, E)

UnmarshalJSON unnmarshals JSON errors into placeholder errors which can then be formatted in the same way as other errors from this package.

Placeholder errors contain same data as original errors (those marshaled into JSON), but have multiple limitations:

  1. They do not implement stackTracer interface because addresses of stack frames are not available in JSON nor they are portable.
  2. Placeholder errors are not of the same type as original errors. Thus errors.Is and errors.As do not work.
  3. The implementation of fmt.Formatter interface of the original error is not used when formatting placeholder errors.

Placeholder errors also have different trees of wrapping errors than original errors because during JSON marshal potentially multiple levels of wrapping are combined into one JSON object. Nested objects happen only for errors implementing causer or unwrapper interface returning multiple errors.

Example
package main

import (
	"encoding/json"
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	base := errors.Base("not found")
	errE := errors.Wrap(base, "image not found")
	errors.Details(errE)["filename"] = "star.png"
	data, err := json.Marshal(errE)
	if err != nil {
		panic(err)
	}
	errFromJSON, errE := errors.UnmarshalJSON(data)
	if errE != nil {
		panic(errE)
	}
	fmt.Printf("% #+-.1v", errFromJSON)

	// Example output:
	// image not found
	// filename=star.png
	// stack trace (most recent call first):
	// gitlab.com/tozd/go/errors_test.ExampleUnmarshalJSON
	// 	/home/user/errors/example_test.go:486
	// testing.runExample
	// 	/usr/local/go/src/testing/run_example.go:63
	// testing.runExamples
	// 	/usr/local/go/src/testing/example.go:44
	// testing.(*M).Run
	// 	/usr/local/go/src/testing/testing.go:1927
	// main.main
	// 	_testmain.go:145
	// runtime.main
	// 	/usr/local/go/src/runtime/proc.go:267
	// runtime.goexit
	// 	/usr/local/go/src/runtime/asm_amd64.s:1650
	//
	// the above error was caused by the following error:
	//
	// not found
}
Output:

func WithDetails added in v0.4.0

func WithDetails(err error, kv ...interface{}) E

WithDetails wraps err into an error which implements the detailer interface to access a map with optional additional details about the error.

If err does not have a stack trace, then this call is equivalent to calling WithStack, annotating err with a stack trace as well.

Use WithDetails when you have an err which implements stackTracer interface but does not implement detailer interface as well. You can also use WithStack for that but you cannot provide initial details using WithStack like you can with WithDetails.

It is also useful when err does implement detailer interface, but you want to reuse same err multiple times (e.g., pass same err to multiple goroutines), adding different details each time. Calling WithDetails always wraps err and adds an additional and independent layer of details on top of any existing details.

You can provide initial details by providing pairs of keys (strings) and values (interface{}).

Example (Printf)
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	base := errors.Base("not found")
	err := errors.WithDetails(base, "file", "plans.txt", "user", "vader")
	fmt.Printf("%#v", err)
}
Output:

not found
file=plans.txt
user=vader

func WithMessage

func WithMessage(err error, prefix ...string) E

WithMessage annotates err with a prefix message or messages. If err does not have a stack trace, stack strace is recorded as well.

It does not support controlling the delimiter. Use Errorf if you need that.

If err is nil, WithMessage returns nil.

WithMessage is similar to Errorf("%s: %w", prefix, err), but supports dynamic number of prefixes, and it returns nil if err is nil.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	cause := errors.New("whoops")
	err := errors.WithMessage(cause, "oh noes")
	fmt.Println(err)
}
Output:

oh noes: whoops
Example (Printf)
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	cause := errors.New("whoops")
	err := errors.WithMessage(cause, "oh noes")
	fmt.Printf("%+-v", err)

	// Example Output:
	// oh noes: whoops
	// stack trace (most recent call first):
	// gitlab.com/tozd/go/errors_test.ExampleWithMessage_printf
	// 	/home/user/errors/example_test.go:46
	// testing.runExample
	// 	/usr/local/go/src/testing/run_example.go:63
	// testing.runExamples
	// 	/usr/local/go/src/testing/example.go:44
	// testing.(*M).Run
	// 	/usr/local/go/src/testing/testing.go:1927
	// main.main
	// 	_testmain.go:131
	// runtime.main
	// 	/usr/local/go/src/runtime/proc.go:267
	// runtime.goexit
	// 	/usr/local/go/src/runtime/asm_amd64.s:1650
}
Output:

func WithMessagef

func WithMessagef(err error, format string, args ...interface{}) E

WithMessagef annotates err with a prefix message formatted according to a format specifier. If err does not have a stack trace, stack strace is recorded as well.

It does not support %w format verb or controlling the delimiter. Use Errorf if you need that.

If err is nil, WithMessagef returns nil.

WithMessagef is similar to Errorf(format + ": %w", args..., err), but it returns nil if err is nil.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	cause := errors.New("whoops")
	err := errors.WithMessagef(cause, "oh noes #%d", 2)
	fmt.Println(err)
}
Output:

oh noes #2: whoops

func WithStack

func WithStack(err error) E

WithStack annotates err with a stack trace at the point WithStack was called, if err does not already have a stack trace. If err is nil, WithStack returns nil.

Use WithStack instead of Wrap when you just want to convert an existing error into one with a stack trace. Use it as close to where the error originated as you can get.

You can also use WithStack when you have an err which implements stackTracer interface but does not implement detailer interface as well, but you cannot provide initial details like you can with WithDetails.

WithStack is similar to Errorf("%w", err), but returns err as-is if err already satisfies interface E and has a stack trace, and it returns nil if err is nil.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	base := errors.Base("whoops")
	err := errors.WithStack(base)
	fmt.Println(err)
}
Output:

whoops
Example (Printf)
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	base := errors.Base("whoops")
	err := errors.WithStack(base)
	fmt.Printf("%+-v", err)

	// Example output:
	// whoops
	// stack trace (most recent call first):
	// gitlab.com/tozd/go/errors_test.ExampleWithStack_printf
	// 	/home/user/errors/example_test.go:85
	// testing.runExample
	// 	/usr/local/go/src/testing/run_example.go:63
	// testing.runExamples
	// 	/usr/local/go/src/testing/example.go:44
	// testing.(*M).Run
	// 	/usr/local/go/src/testing/testing.go:1927
	// main.main
	// 	_testmain.go:131
	// runtime.main
	// 	/usr/local/go/src/runtime/proc.go:267
	// runtime.goexit
	// 	/usr/local/go/src/runtime/asm_amd64.s:1650
}
Output:

func Wrap

func Wrap(err error, message string) E

Wrap returns an error annotating err with a stack trace at the point Wrap is called, and the supplied message. Wrapping is done even if err already has a stack trace. It records the original error as a cause. If err is nil, Wrap returns nil.

Use Wrap when you want to make a new error with a different error message, while preserving the cause of the new error. If you want to reuse the err error message use WithMessage or Errorf instead.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	cause := errors.New("whoops")
	err := errors.Wrap(cause, "oh noes")
	fmt.Println(err)
}
Output:

oh noes
Example (Printf)
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	cause := errors.New("whoops")
	err := errors.Wrap(cause, "oh noes")
	fmt.Printf("% +-.1v", err)

	// Example output:
	// oh noes
	// stack trace (most recent call first):
	// gitlab.com/tozd/go/errors_test.ExampleWrap_printf
	// 	/home/user/errors/example_test.go:116
	// testing.runExample
	// 	/usr/local/go/src/testing/run_example.go:63
	// testing.runExamples
	// 	/usr/local/go/src/testing/example.go:44
	// testing.(*M).Run
	// 	/usr/local/go/src/testing/testing.go:1927
	// main.main
	// 	_testmain.go:131
	// runtime.main
	// 	/usr/local/go/src/runtime/proc.go:267
	// runtime.goexit
	// 	/usr/local/go/src/runtime/asm_amd64.s:1650
	//
	// the above error was caused by the following error:
	//
	// whoops
	// stack trace (most recent call first):
	// gitlab.com/tozd/go/errors_test.ExampleWrap_printf
	// 	/home/user/errors/example_test.go:115
	// testing.runExample
	// 	/usr/local/go/src/testing/run_example.go:63
	// testing.runExamples
	// 	/usr/local/go/src/testing/example.go:44
	// testing.(*M).Run
	// 	/usr/local/go/src/testing/testing.go:1927
	// main.main
	// 	_testmain.go:131
	// runtime.main
	// 	/usr/local/go/src/runtime/proc.go:267
	// runtime.goexit
	// 	/usr/local/go/src/runtime/asm_amd64.s:1650
}
Output:

func WrapWith added in v0.6.0

func WrapWith(err, with error) E

WrapWith makes the "err" error the cause of the "with" error. This is similar to Wrap but instead of using just an error message, you can provide a base error instead. If "err" is nil, WrapWith returns nil. If "with" is nil, WrapWith panics.

If the "with" error does already have a stack trace, a stack trace is recorded at the point WrapWith was called.

The new error wraps two errors, the "with" error and the "err" error, making it possible to use both Is and As on the new error to traverse both "with" and "err" errors at the same time.

Note that the new error introduces a new context for details so any details from the "err" and "with" errors are not available through AllDetails on the new error.

Use WrapWith when you want to make a new error using a base error with a different error message, while preserving the cause of the new error. If you want to reuse the "err" error message use Prefix or Errorf instead.

Example
package main

import (
	"fmt"
	"io"

	"gitlab.com/tozd/go/errors"
)

func main() {
	cause := io.EOF
	baseErr := errors.Base("an error")

	err := errors.WrapWith(cause, baseErr)
	fmt.Println(err)
}
Output:

an error
Example (Printf)
package main

import (
	"fmt"
	"io"

	"gitlab.com/tozd/go/errors"
)

func main() {
	cause := io.EOF
	baseErr := errors.Base("an error")

	err := errors.WrapWith(cause, baseErr)
	fmt.Printf("% +-.1v", err)

	// Example output:
	// an error
	// stack trace (most recent call first):
	// gitlab.com/tozd/go/errors_test.ExampleWrapWith_printf
	// 	/home/user/errors/example_test.go:536
	// testing.runExample
	// 	/usr/local/go/src/testing/run_example.go:63
	// testing.runExamples
	// 	/usr/local/go/src/testing/example.go:44
	// testing.(*M).Run
	// 	/usr/local/go/src/testing/testing.go:1927
	// main.main
	// 	_testmain.go:155
	// runtime.main
	// 	/usr/local/go/src/runtime/proc.go:267
	// runtime.goexit
	// 	/usr/local/go/src/runtime/asm_amd64.s:1650
	//
	// the above error was caused by the following error:
	//
	// EOF
}
Output:

func Wrapf

func Wrapf(err error, format string, args ...interface{}) E

Wrapf returns an error annotating err with a stack trace at the point Wrapf is called, and the supplied message formatted according to a format specifier. Wrapping is done even if err already has a stack trace. It records the original error as a cause. It does not support %w format verb (use %s instead if you need to incorporate cause's error message). If err is nil, Wrapf returns nil.

Use Wrapf when you want to make a new error with a different error message, preserving the cause of the new error. If you want to reuse the err error message use WithMessage or Errorf instead.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	base := errors.Base("whoops")
	err := errors.Wrapf(base, "oh noes #%d", 2)
	fmt.Println(err)
}
Output:

oh noes #2

type Formatter added in v0.6.0

type Formatter struct {
	Error error

	// Provide a function to obtain the error's message.
	// By default error's Error() is called.
	GetMessage func(error) string `exhaustruct:"optional"`
}

Formatter formats an error as text and marshals the error as JSON.

func (Formatter) Format added in v0.6.0

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

Format formats the error as text according to the fmt.Formatter interface.

The error does not have to necessary come from this package and it will be formatted in the same way if it implements interfaces used by this package (e.g., stackTracer or detailer interfaces). By default, only if those interfaces are not implemented, but fmt.Formatter interface is, formatting will be delegated to the error itself. You can change this default through format precision.

Errors which do come from this package can be directly formatted by the fmt package in the same way as this function does as they implement fmt.Formatter interface. If you are not sure about the source of the error, it is safe to call this function on them as well.

The following verbs are supported:

%s    the error message
%q    the quoted error message
%v    by default the same as %s

You can control how is %v formatted through the width and precision arguments and flags. The width argument controls the width of the indent step in spaces. The default (no width argument) indents with a tab step. Width is passed through to the stack trace formatting.

The following flags for %v are supported:

'#'   list details as key=value lines after the error message, when available
'+'   follow with the %+v formatted stack trace, if available
'-'   add human friendly messages to delimit parts of the text
' '   add extra newlines to separate parts of the text better

Precision is specified by a period followed by a decimal number and enable modes of operation. The following modes are supported:

.0    do not change default behavior, this is the default
.1    recurse into error causes and joined errors
.2    prefer error's fmt.Formatter interface implementation if error implements it
.3    recurse into error causes and joined errors, but prefer fmt.Formatter
      interface implementation if any error implements it; this means that
      recursion stops if error's formatter does not recurse

When any flag or non-zero precision mode is used, it is assured that the text ends with a newline, if it does not already do so.

Example
package main

import (
	"fmt"

	"gitlab.com/tozd/go/errors"
)

func main() {
	base := errors.Base("not found")
	err := errors.Wrap(base, "image not found")
	errors.Details(err)["filename"] = "star.png"
	fmt.Printf("% #+-.1v", err)

	// Example output:
	// image not found
	// filename=star.png
	// stack trace (most recent call first):
	// gitlab.com/tozd/go/errors_test.ExampleFormatter_Format
	// 	/home/user/errors/example_test.go:395
	// testing.runExample
	// 	/usr/local/go/src/testing/run_example.go:63
	// testing.runExamples
	// 	/usr/local/go/src/testing/example.go:44
	// testing.(*M).Run
	// 	/usr/local/go/src/testing/testing.go:1927
	// main.main
	// 	_testmain.go:137
	// runtime.main
	// 	/usr/local/go/src/runtime/proc.go:267
	// runtime.goexit
	// 	/usr/local/go/src/runtime/asm_amd64.s:1650
	//
	// the above error was caused by the following error:
	//
	// not found
}
Output:

func (Formatter) MarshalJSON added in v0.6.0

func (f Formatter) MarshalJSON() ([]byte, error)

MarshalJSON marshals the error as JSON according to the json.Marshaler interface.

The error does not have to necessary come from this package and it will be marshaled in the same way if it implements interfaces used by this package (e.g., stackTracer or detailer interfaces). Only if those interfaces are not implemented, but json.Marshaler interface is or the error is a struct with JSON struct tags, marshaling will be delegated to the error itself.

Errors which do come from this package can be directly marshaled in the same way as this function does as they implement json.Marshaler interface. If you are not sure about the source of the error, it is safe to call this function on them as well.

Example
package main

import (
	"bytes"
	"encoding/json"
	"os"

	"gitlab.com/tozd/go/errors"
)

func main() {
	base := errors.Base("not found")
	errE := errors.Wrap(base, "image not found")
	errors.Details(errE)["filename"] = "star.png"
	data, err := json.Marshal(errE)
	if err != nil {
		panic(err)
	}
	out := new(bytes.Buffer)
	_ = json.Indent(out, data, "", "\t")
	_, _ = out.WriteTo(os.Stdout)

	// Example output:
	// {
	// 	"cause": {
	// 		"error": "not found"
	// 	},
	// 	"error": "image not found",
	// 	"filename": "star.png",
	// 	"stack": [
	// 		{
	// 			"name": "gitlab.com/tozd/go/errors_test.ExampleFormatter_MarshalJSON",
	// 			"file": "/home/user/errors/example_test.go",
	// 			"line": 427
	// 		},
	// 		{
	// 			"name": "testing.runExample",
	// 			"file": "/usr/local/go/src/testing/run_example.go",
	// 			"line": 63
	// 		},
	// 		{
	// 			"name": "testing.runExamples",
	// 			"file": "/usr/local/go/src/testing/example.go",
	// 			"line": 44
	// 		},
	// 		{
	// 			"name": "testing.(*M).Run",
	// 			"file": "/usr/local/go/src/testing/testing.go",
	// 			"line": 1927
	// 		},
	// 		{
	// 			"name": "main.main",
	// 			"file": "_testmain.go",
	// 			"line": 145
	// 		},
	// 		{
	// 			"name": "runtime.main",
	// 			"file": "/usr/local/go/src/runtime/proc.go",
	// 			"line": 267
	// 		},
	// 		{
	// 			"name": "runtime.goexit",
	// 			"file": "/usr/local/go/src/runtime/asm_amd64.s",
	// 			"line": 1650
	// 		}
	// 	]
	// }
}
Output:

type StackFormatter added in v0.6.0

type StackFormatter struct {
	Stack []uintptr
}

StackFormatter formats a stack trace as text and marshals the stack trace as JSON.

func (StackFormatter) Format added in v0.6.0

func (s StackFormatter) Format(st fmt.State, verb rune)

Format formats the stack of frames as text according to the fmt.Formatter interface.

The stack trace can come from errors in this package, from runtime.Callers, or from somewhere else.

Each frame in the stack is formatted according to the format and is ended by a newline.

The following verbs are supported:

%s    lists the source file basename
%d    lists the source line number
%n    lists the short function name
%v    equivalent to %s:%d

StackFormat accepts flags that alter the formatting of some verbs, as follows:

%+s   lists the full function name and full compile-time path of the source file,
      separated by \n\t (<funcname>\n\t<path>)
%+v   lists the full function name and full compile-time path of the source file
      with the source line number, separated by \n\t
      (<funcname>\n\t<path>:<line>)

StackFormat also accepts the width argument which controls the width of the indent step in spaces. The default (no width argument) indents with a tab step.

Example
package main

import (
	"fmt"
	"runtime"

	"gitlab.com/tozd/go/errors"
)

func main() {
	const depth = 1
	var cs [depth]uintptr
	runtime.Callers(1, cs[:])
	fmt.Printf("%+v", errors.StackFormatter{cs[:]})

	// Example output:
	// gitlab.com/tozd/go/errors_test.ExampleStackFormatter_Format
	// 	/home/user/errors/example_test.go:374
}
Output:

Example (Width)
package main

import (
	"fmt"
	"runtime"

	"gitlab.com/tozd/go/errors"
)

func main() {
	const depth = 1
	var cs [depth]uintptr
	runtime.Callers(1, cs[:])
	fmt.Printf("%+2v", errors.StackFormatter{cs[:]})

	// Example output:
	// gitlab.com/tozd/go/errors_test.ExampleStackFormatter_Format
	//   /home/user/errors/example_test.go:385
}
Output:

func (StackFormatter) MarshalJSON added in v0.6.0

func (s StackFormatter) MarshalJSON() ([]byte, error)

MarshalJSON marshals the stack of frames as JSON according to the json.Marshaler interface.

JSON consists of an array of frame objects, each with (function) name, file (name), and line fields.

Example
package main

import (
	"encoding/json"
	"fmt"
	"runtime"

	"gitlab.com/tozd/go/errors"
)

func main() {
	const depth = 1
	var cs [depth]uintptr
	runtime.Callers(1, cs[:])
	data, err := json.Marshal(errors.StackFormatter{cs[:]})
	if err != nil {
		panic(err)
	}
	fmt.Println(string(data))

	// Example output:
	// [{"name":"gitlab.com/tozd/go/errors_test.ExampleStackFormatter_MarshalJSON","file":"/home/user/errors/example_test.go","line":360}]
}
Output:

type StackTrace added in v0.6.0

type StackTrace = []uintptr

StackTrace is a type alias for better compatibility with github.com/pkg/errors. It does not define a new type.

Jump to

Keyboard shortcuts

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