Categorygithub.com/segmentio/errors-go
modulepackage
1.0.0
Repository: https://github.com/segmentio/errors-go.git
Documentation: pkg.go.dev

# README

errors-go CircleCI Go Report Card GoDoc

Motivations

Error management in Go is very flexible, the builtin error interface only require the error value to expose an Error() string method which gives a human-readable representation of what went wrong. A typical way to report errors is through the use of constant values that a program can compare error values returned by functions against to determine the issue (this is the model used by io.EOF for example).

However, this is not very flexible and creates a hard dependency between the programs and the packages they use. If a package's implementation changes the error value returned under some circumstances, or adds new errors, the programs depending on that package may need to be updated to adapt to the package's new behavior.

One approach that spread across the standard library to loosen the dependency between the package producing the errors and the program handling them has been to establish convention on interfaces that the actual error type implements. This allows the program to ask questions about the error value it received, like whether or not it's a temporary error, or if it happened because of a timeout (because the error type exposes Temporary() bool and Timeout() bool methods). This approach offers strong decoupling between components of a program and is one of the pillar concepts that this package is built upon.

Another limitation of carrying a single error value is that it doesn't allow layers of abstractions to add and carry context about the consequences of an error. When io.ErrUnexpectedEOF is returned by a low-level I/O routine, the next abstraction layer can either propagate the error, or return a different error value, the former gives no information about the consequence of the error, the latter loses information of what the original error was.

Packages like github.com/pkg/errors attempt to provide more powerful tools to compose errors and aggregate context about the cause and consequences of those errors, but they are by choice of their authors narrowed to solving a single aspect of error management.

This is where the errors-go package comes into play. It is built to be a drop-in replacement for pkg/errors while offering a wider set of error management tools that we wished we had countless times in order to build software that is more robust, expressive, and maintainable.

Types

In the errors-go package, errors can carry types, and a program can dynamically test what types an error value has. Error types are methods that take no arguments and return a boolean value. This is the same mechanism used in the standard library to test whether errors are temporary, or if they happened because of a timeout.

For example, this error may be of type Temporary, Timeout, and Unreachable

type myError struct {
    unreachable bool
    timeout     bool
}

func (e *myError) Error()       string { return "..." }
func (e *myError) Temporary()   bool   { return true }
func (e *myError) Timeout()     bool   { return e.timeout }
func (e *myError) Unreachable() bool   { return e.unreachable }

and a program may use the errors.Is(typ string, err error) bool function to test what types the error has

switch {
case errors.Is("Timeout", err):
    ...
case errors.Is("Unreachable", err):
    ...
default:
    ...
}

errors.Is dynamically discovers whether the error has a method matching the type name, and calls it to test the error type.

Resources:

Tagging

Tags are a list of arbitrary key/value pairs that can be attached to any errors, it provides a way to aggregate errors based on tag values. They are useful when errors are logged, traced, or measured, since the tags can be extracted from the error and injected into the logging, tracing, or metric collection systems.

To add tags to an error, a program may either define a Tags() []errors.Tag method on an error type, or use the errors.WithTags function to add tags to an error value, for example:

operation := "HelloWorld"

if err := rpc.Call(operation); err != nil {
    return errors.WithTags(err,
        errors.T("component", "rpc-client"),
        errors.T("operation", operation),
    )
}

Resources:

Causes

When wrapping an error value to add context to it (may it be types, tags, or other attributes), a program can retrieve the original error by using the errors.Cause(error) error function. This mechanism is identical to what is done in the github.com/pkg/errors package and is useful when a program needs to compare the original error value against some constant like io.EOF for example.

However, it is common in Go to end up with more than one cause for an error. When a program spawns multiple goroutine to do I/O operations in parallel, some may succeed and some may fail. Instead of having to discard all errors but the first one, or re-invent yet another multi-error type, the errors-go package offers two methods to construct error values from a set of errors:

func Join(errs ...error) error
func Recv(errs <-chan error) error

A program then cannot count on retrieving the single cause and instead can use the errors.Causes(error) []error function to extract all causes that lead to generating this error.

This means that the errors-go package allows programs to build error trees, where each node is an error value (including wrappers) with a list of causes, that may as well have been wrapped, and may also have causes themselves.

Resources:

Adapters

Due to Go's very flexible error handling model, packages have adopted different approaches, which are not always easy to plug together. This is true even within the standard library itself, where some packages use specific error types, exported or unexported error values, or a combination of those. Those differences result in heterogenous error handling code within a single program.

To work around this issue the errors-go package uses the Adapter concept. An Adapter is meant to convert errors generated by a package into errors that can be manipulated using the errors-go functions.

Adapters are intended to be registered during the initialization phase of a package (using its init function) in order to be available globally whenever an error needs to be adapted.

Errors are adapted automatically by calls to any of the wrapper functions of the errors-go package (like Wrap, WithMessage, WithStack, etc...). It means that all a program needs to do is import adapter packages and its error wrapping functions will be enhanced to do proper error decoration of errors coming from those packages.

Resources:

Formatting

Errors are eventually meant to be consumed by developers and operators of a program, which means formatting of the error values into human-readable forms is a key feature of an error package.

Go has understood this well by making the error interface's single method one that returns an error message. However, a single error message often isn't enough to communicate all the context of what went wrong, and using a format that allows the context to be fully exposed is highly valuable, both during development and operations.

Errors wrapped by, or produced by the errors-go package all use a text format which exposes the message, types, tags, stack traces, and the tree of causes that resulted in the error. Those information can be enabled or disabled based on the format string being used (e.g. %v, %+v, %#v), here's an example of the full format:

error message (type ...) [tag=value ...]
stack traces
...
├── sub error message (type ...) [tag=value ...]
|   stack traces
|   ...
└── last error message (type ...) [tag=value ...]
    stack traces
  • The first line is the error message, the types and tags of the error.
  • The stack traces are printed in the same format as the panic stack traces.
  • The tree-like format provides a representation of the tree of error causes.

This format ensures that all information carried by the error are available in a way that is both familiar with current practices (Go debug traces, format of errors from the github.com/pkg/errors package, ...) while still exposing other properties of errors of the errors-go package.

Serializing

Serializability is not a property that's very easy to obtain from Go errors. Because the standard error interface only gives access to an error message one can simply serialize this message to pass errors across services over the network, but this process would lose other information like types, tags, or causes.

To address this limitation the errors-go package uses an intermediary, serializable type to represent errors, which can also be used as a form of reflection to explore the inner structure of an error value.

The errors.ValueOf(error) function returns a errors.Value which snapshots a representation of the error passed as argument. Values can then be manipulated and serialized and deserialized by an program, and an error carrying the same properties as the original can be reconstructed from the value.

Note: The only property of an error which is not equivalent in errors built from values and their original is the stack trace. This design choice was made because programs exchanging error information over the network rarely need to carry a stack trace of a different code than theirs. So instead, the stack trace in errors that are reconstructed from values is a new capture of the call stack within the current program.

Resources:

# Packages

Package awserrors provides functions to adapt errors of the AWS Go SDK into errors compatible with the errors-go package.
No description provided by the author
Package httperrors provides functions to construct errors from HTTP responses.
Package ioerrors provides adapters for errors generated by the standard io package.
Package neterrors provides adapters for errors generated by the standard net package.
Package pkgerrors provides adapters for errors generated by the github.com/pkg/errors package.
Package stderrors exposes no APIs and is used for the sole purpose of setting up global adapters for all packages of the standard library supported by the errors-go project.
Package twirperrors provides functions to adapt errors of the github.com/twitchtv/twirp package into errors compatible with the errors-go package.

# Functions

Adapt adapts err using the registered adapters.
CaptureStackTrace walks the call stack that led to this function and records it as a StackTrace value.
Cause returns the cause of err, which may be err if it had no cause.
Causes returns the list of causes of err, which may be an empty slice if err is nil or had no causes.
Err constructs an error from a value of arbitrary type, using the following rules: - if v is nil, the function simply returns nil - if v is a string type, the function behaves like calling New - if v is already an error it is returned unchanged - if v has an Err() method returning an error, the function returns the result of calling it.
Errorf returns an error that formats as fmt.Sprintf(msg, args...).
Inspect extract and returns properties of err.
Is tests whether err is of type typ.
Join composes an error from the list of errors passed as argument.
LookupTag returns value for a given tag name.
New returns an error that formats as the given message.
Recv reads all errors from the given channel and returns one that combines them.
Register registers a new error adapter.
T returns a Tag value with the given name and value.
Tags returns a slice containing all the tags set on err and its causes (it if had any).
Types returns a slice containing all the types implemented by err and its causes (if it had any).
ValueOf returns an error value representing err.
WithMessage returns an error that wraps err and prefix its original error error message with msg.
WithStack returns an error that wraps err with a capture of the stack trace at the time the function is called.
WithStackTrace returns an error that wraps err with the given stack trace.
WithTags returns an error that wraps err and tags it with the given key/value pairs.
WithTypes returns an error that wraps err and implements the given types so that calling errors.Is on the returned error with one of the given types will return true.
Wrap returns an error that wraps err with msg as prefix to its original message and a capture of the stack trace at the time the function is called.
Wrapf returns an error that wraps err with fmt.Sprintf(msg, args...) as prefix to its original message and a capture of the stack trace at the time the function is called.

# Variables

TODO is a non-nil error intended to act as a placeholder during development when writing the structure of the code but the implementation is still left to be written.

# Structs

Tag is a key/value type used to represent a single error tag.
Value is a serializable error representation which carries all rich information of errors that can be constructed by this package.

# Interfaces

Adapter is an interface implemented by types that support adapting errors to be introspected by functions of the erorrs package.

# Type aliases

The AdapterFunc types is an implementation of the Adapter interface which makes it possible to use simple functions as error adapters.
Frame represents a program counter inside a stack frame.
StackTrace is stack of Frames from innermost (newest) to outermost (oldest).