errors

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Dec 12, 2019 License: MIT Imports: 7 Imported by: 0

README

go-errors Travis Build Badge Go Report Card Badge GoDoc Badge

This package aims to provide flexible general-purpose error handling functionality, with the following specific features in mind:

  • Error wrapping: Allowing an error's cause to be preserved along with additional information.
  • Stack tracing: Allowing the path taken to return an error to be easily identified.
  • Structured fields: Allowing errors to be logged with additional contextual information.

This library has built upon the mistakes we've made and lessons we've learnt with regards to error handling at Icelolly whilst working on our internal APIs. This library was inspired by approaches found elsewhere in the community, most notably the approach found in Upspin.

Usage

Creating and Wrapping Errors

Errors may have a few different pieces of information attached to them; an errors.Kind, a message, and fields. All of these things are optional, but at least an errors.Kind or message must be given if using errors.New. Along with this information, file and line information will be added automatically. If you're wrapping an error, the only thing you must pass is an error to wrap as the first argument:

const ErrInvalidName errors.Kind = "auth: user's name is invalid"

func persistUser(user *User) error {
    if user.Name == "" {
        // Error kinds like `ErrInvalidName` can be used to react to different 
        // errors to decide how to handle them at the caller.
        return errors.New(ErrInvalidName)
    }
    
    err := persist(user)
    if err != nil {
        // Wrapping errors let's you add contextual information which may be
        // extracted again later (e.g. for logging).
        return errors.Wrap(err, "auth: failed to persist user in DB").
            WithField("user", user)
    }
    
    return nil
}
Handling Errors

Most error handling is done using errors.Is, which checks if the given error is of a given errors.Kind. If the error doesn't have it's own errors.Kind, then errors.Is will look through the errors stack until it finds an errors.Kind:

func registerUserHandler(w http.ResponseWriter, r *http.Request) {
    user := // ...

    err := persistUser(user)
    switch {
    case errors.Is(err, ErrInvalidName):
        http.Error(w, "user has invalid username", 400)
        return
    case err != nil:
        http.Error(w, http.StatusText(500), 500)
        return
    }
    
    // ...
}

A more thorough example of usage can be found in the example/ directory. It showcases creating errors, wrapping them, handling different kinds of errors, and dealing with things like logging.

See More

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Fatal

func Fatal(err error)

Fatal will panic if given a non-nil error. If the given error is an *Error, the output format of the panic will be slightly different, so as to include as much relevant information as possible in an easy format for operators to digest. If a regular error is given, it will simply be passed to panic as normal.

func Fields

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

Fields returns all fields from all errors in a stack of errors, recursively checking for fields and merging them into one map, then returning them.

func FieldsSlice

func FieldsSlice(err error) []interface{}

FieldsSlice returns all fields from all errors in a stack of errors, recursively checking for fields and merging them into one slice, then returning them. This function uses Fields internally, so the behaviour is very similar. The returned slice is ordered by key, so calling this function produces consistent results.

func Is

func Is(err error, kind ...interface{}) bool

Is reports whether the err is an *Error of the given kind/value. If the given kind is of type Kind/string, it will be checked against the error's Kind. If the given kind is of any other type, it will be checked against the error's cause. This is done recursively until a matching error is found. Calling Is with multiple kinds reports whether the error is one of the given kind/values, not all of.

func Message

func Message(err error) string

Message returns what is supposed to be a human-readable error message. It is designed to not leak internal implementation details (unlike calling *Error.Error()). If the given error is not an *Error, then a generic message will be returned. If the given error is nil, then an empty string will be returned.

Types

type Error

type Error struct {
	// Kind can be used as a sort of pseudo-type that check on. It's a useful mechanism for avoiding
	// "sentinel" errors, or for checking an error's type. Kind is defined as a string so that error
	// kinds can be defined in other packages.
	Kind Kind

	// Message is a human-readable, user-friendly string. Unlike caller, Message is really intended
	// to be user-facing, i.e. safe to send to the front-end.
	Message string

	// Cause is the previous error. The error that triggered this error. If it is nil, then the root
	// cause is this Error instance. If Cause is not nil, but also not of type Error, then the root
	// cause is the error in Cause.
	Cause error

	// Fields is a general-purpose map for storing key/value information. Useful for providing
	// additional structured information in logs.
	Fields map[string]interface{}
	// contains filtered or unexported fields
}

Error is a general-purpose error type, providing much more contextual information and utility when compared to the built-in error interface.

func New

func New(args ...interface{}) *Error

New returns a new error. New accepts a variadic list of arguments, but at least one argument must be specified, otherwise New will panic. New will also panic if an unexpected type is given to it. Each field that can be set on an *Error is of a different type, meaning we can switch on the type of each argument, and still know which field to set on the error, leaving New as a very flexible function that is also not overly verbose to call.

Example usage:

// Create the initial error, maybe this would be returned from some function.
err := errors.New(ErrKindTimeout, "client: HTTP request timed out")
// Wrap an existing error. It can be a regular error too. Also, set a field.
err = errors.New(err, "accom: fetch failed", errors.WithField("tti_code", ttiCode))

As you can see, this usage is flexible, and includes the ability to construct pretty much any kind of error your application should need.

func Wrap

func Wrap(cause error, args ...interface{}) *Error

Wrap constructs an error the same way that New does, the only difference being that if the given cause is nil, this function will return nil. This makes it quite handy in return lines at the end of functions. Wrap conveys it's meaning a little more than New does when you are wrapping other errors.

func (*Error) Error

func (e *Error) Error() string

Error satisfies the standard library's error interface. It returns a message that should be useful as part of logs, as that's where this method will likely be used most, including the caller, and the message, for the whole stack.

func (*Error) Format

func (e *Error) Format(s fmt.State, c rune)

Format allows this error to be formatted differently, depending on the needs of the developer. The different formatting options made available are:

%v: Standard formatting: shows callers, and shows messages, for the whole stack. %+v: Verbose formatting: shows callers, and shows messages, for the whole stack, with file and

line, information, across multiple lines.

func (*Error) WithField

func (e *Error) WithField(fieldKey string, fieldValue interface{}) *Error

WithField appends a key/value pair to the error's field list.

func (*Error) WithFields

func (e *Error) WithFields(kvs ...interface{}) *Error

WithFields appends a set of key/value pairs to the error's field list.

type Kind

type Kind string

Kind is simply a string, but it allows New to function the way it does, and limits what can be passed as the kind of an error to things defined as actual error kinds.

type StackFrame

type StackFrame struct {
	Kind    string                 `json:"kind,omitempty"`
	Message string                 `json:"message,omitempty"`
	Caller  string                 `json:"caller,omitempty"`
	File    string                 `json:"file,omitempty"`
	Line    int                    `json:"line,omitempty"`
	Fields  map[string]interface{} `json:"fields,omitempty"`
}

StackFrame represents a single error in a stack of errors. All fields could be empty, because we may even be dealing with a regular error.

func Stack

func Stack(err error) []StackFrame

Stack produces a slice of StackFrame structs that can easily be encoded to JSON. The main intended use of this function is for logging, so that you can attach a stack trace to a log entry to help track down the cause of an error.

This function looks a little more complex than some of the other recursive alternatives, but because of the nature of slices, this implementation is considerably faster than using a recursive solution (i.e. this only has 1 allocation, whereas a recursive solution may have 1 or 2 allocations per stack frame).

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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