Documentation ¶
Overview ¶
Package errors provides simple error handling primitives that work well with structured logging.
This package is inspired by the excellent github.com/pkg/errors package. A significant amount of code and documentation in this package has been adapted from that source.
A key difference between this package and github.com/pkg/errors is that this package has been designed to suit programs that make use of structured logging. Some of the ideas in this package were proposed for package github.com/pkg/errors, but after a reasonable amount of consideration, were ultimately not included in that package. (See https://github.com/pkg/errors/issues/34 for details).
If you are not using structured logging in your application and have no intention of doing so, you will probably be better off using the github.com/pkg/errors package in preference to this one.
Background ¶
The traditional error handling idiom in Go is roughly akin to
if err != nil { return err }
which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error.
Creating errors ¶
The `errors` package provides three operations which combine to form a simple yet powerful system for enhancing the value of returned errors:
New create a new error Wrap wrap an existing error with an optional message With attach key/value pairs to an error
The `New` function is used to create an error. This function is compatible with the Go standard library `errors` package:
err := errors.New("emit macho dwarf: elf header corrupted")
The `Wrap` function returns an error that adds a message to the original error. This additional message can be useful for putting the original error in context. For example:
err := errors.New("permission denied") fmt.Println(err) err = errors.Wrap(err, "cannot list directory contents") fmt.Println(err) // Output: // permission denied // cannot list directory contents: permission denied
The `With` function accepts a variadic list of alternating key/value pairs, and returns an error context that can be used to create a new error or wrap an existing error.
// create new error err = errors.With("file", "testrun", "line", 101).New("file locked") fmt.Println(err) // wrap existing error err = errors.With("attempt", 3).Wrap(err, "retry failed") fmt.Println(err) // Output: // file locked file=testrun line=101 // retry failed attempt=3: file locked file=testrun line=101
One useful pattern is to create an error context that is used for an entire function scope:
func doSomethingWith(file string, line int) error { // set error context errors := errors.With("file", file, "line", line) if number <= 0 { // file and line will be attached to the error return errors.New("invalid number") } // ... later ... if err := doOneThing(); err != nil { // file and line will be attached to the error return errors.Wrap(err, "cannot do one thing") } // ... and so on until ... return nil }
The errors returned by `New` and `Wrap` provide a `With` method that enables a fluent-style of error handling:
// create new error err = errors.New("file locked").With( "file", "testrun", "line", 101, ) fmt.Println(err) // wrap existing error err = errors.Wrap(err, "retry failed").With("attempt", 3) fmt.Println(err) // Output: // file locked file=testrun line=101 // retry failed attempt=3: file locked file=testrun line=101
Retrieving the cause of an error ¶
Using errors.Wrap constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by errors.Cause.
type causer interface { Cause() error }
errors.Cause will recursively retrieve the topmost error which does not implement causer, which is assumed to be the original cause. For example:
switch err := errors.Cause(err).(type) { case *MyError: // handle specifically default: // unknown error }
Retrieving key value pairs for structured logging ¶
Errors created by `errors.Wrap` and `errors.New` implement the following interface:
type keyvalser interface { Keyvals() []interface{} }
The Keyvals method returns an array of alternating keys and values. The first key will always be "msg" and its value will be a string containing the message associated with the wrapped error.
Example using go-kit logging (https://github.com/go-kit/kit/tree/master/log):
// logError logs details of an error to a structured error log. func logError(logger log.Logger, err error) { // start with timestamp and error level keyvals := []interface{}{ "ts", time.Now().Format(time.RFC3339Nano), "level", "error", } type keyvalser interface { Keyvals() []interface{} } if kv, ok := err.(keyvalser); ok { // error contains structured information, first key/value // pair will be "msg". keyvals = append(keyvals, kv.Keyvals()...) } else { // error does not contain structured information, use the // Error() string as the message. keyvals = append(keyvals, "msg", err.Error()) } logger.Log(keyvals...) }
GOOD ADVICE: Do not use the Keyvals method on an error to retrieve the individual key/value pairs associated with an error for processing by the calling program.
Example ¶
package main import ( "fmt" "github.com/jjeffery/errors" ) func main() { err := errors.New("first error").With( "card", "ace", "suite", "spades", ) fmt.Println(err) err = errors.Wrap(err, "second error").With( "piece", "rook", "color", "black", ) fmt.Println(err) }
Output: first error card=ace suite=spades second error piece=rook color=black: first error card=ace suite=spades
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Cause ¶
Cause returns the underlying cause of the error, if possible. An error value has a cause if it implements the following interface:
type causer interface { Cause() error }
If the error does not implement Cause, the original error will be returned. If the error is nil, nil will be returned without further investigation.
Cause is compatible with the Cause function in package "github.com/pkg/errors". The implementation and documentation of Cause has been copied from that package.
Example ¶
package main import ( "fmt" "github.com/jjeffery/errors" ) func main() { // tests if an error is a not found error type notFounder interface { NotFound() bool } err := getError() if notFound, ok := errors.Cause(err).(notFounder); ok { fmt.Printf("Not found: %v", notFound.NotFound()) } } func getError() error { return fmt.Errorf("not a not found error") }
Output:
Types ¶
type Context ¶
type Context interface { With(keyvals ...interface{}) Context New(message string) Error Wrap(err error, message ...string) Error }
A Context contains key/value pairs that will be attached to any error created or wrapped from that context.
One useful pattern applies to functions that can return errors from many places. Define an `errors` variable early in the function:
func doSomethingWith(id string, n int) error { // defines a new context with common key/value pairs errors := errors.With("id", id, "n", n) // ... later on ... if err := doSomething(); err != nil { return errors.Wrap(err, "cannot do something") } // ... and later still ... if somethingBadHasHappened() { return errors.New("something bad has happened") } // ... and so on ...
This pattern ensures that all errors created or wrapped in a function have the same key/value pairs attached.
func With ¶
func With(keyvals ...interface{}) Context
With creates a context with the key/value pairs.
Example ¶
package main import ( "fmt" "github.com/jjeffery/errors" ) var userID = "u1" var documentID = "d1" func main() { // ... if a function has been called with userID and DocumentID ... errors := errors.With("userID", userID, "documentID", documentID) n, err := doOneThing() if err != nil { // will include key value pairs for userID and document ID fmt.Println(errors.Wrap(err, "cannot do one thing")) } if err := doAnotherThing(n); err != nil { // will include key value pairs for userID, document ID and n fmt.Println(errors.Wrap(err, "cannot do another thing").With("n", n)) } if !isValid(userID) { // will include key value pairs for userID and document ID fmt.Println(errors.New("invalid user")) } } func doOneThing() (int, error) { return 0, fmt.Errorf("doOneThing: unable to finish") } func doAnotherThing(n int) error { return fmt.Errorf("doAnotherThing: not working properly") } func isValid(s string) bool { return false }
Output: cannot do one thing userID=u1 documentID=d1: doOneThing: unable to finish cannot do another thing userID=u1 documentID=d1 n=0: doAnotherThing: not working properly invalid user userID=u1 documentID=d1
type Error ¶
The Error interface implements the builtin error interface, and implements an additional method that attaches key value pairs to the error.
func New ¶
New returns a new error with a given message.
Example ¶
package main import ( "fmt" "github.com/jjeffery/errors" ) func getNameOfThing() string { return "!not-valid" } func isValidName(name string) bool { return false } func main() { name := getNameOfThing() if !isValidName(name) { fmt.Println(errors.New("invalid name").With("name", name)) } }
Output: invalid name name="!not-valid"
func Wrap ¶
Wrap creates an error that wraps an existing error. If err is nil, Wrap returns nil.
Example ¶
package main import ( "fmt" "github.com/jjeffery/errors" ) func doSomething() error { return fmt.Errorf("not implemented") } func doSomethingWith(name string) error { return fmt.Errorf("permission denied") } func main() { if err := doSomething(); err != nil { fmt.Println(errors.Wrap(err, "cannot do something")) } name := "otherthings.dat" if err := doSomethingWith(name); err != nil { fmt.Println(errors.Wrap(err, "cannot do something with").With("name", name)) } }
Output: cannot do something: not implemented cannot do something with name="otherthings.dat": permission denied
Notes ¶
Bugs ¶
This package makes use of a fluent API for attaching key/value pairs to an error. Dave Cheney has written up some good reasons to avoid this approach: see https://github.com/pkg/errors/issues/15#issuecomment-221194128. Experience will show if this presents a problem, but to date it has felt like it leads to simpler, more readable code.
Attaching key/value pairs to an error was considered for package github.com/pkg/errors, but in the end it was not implemented because of the potential for abusing the information in the error. See Dave Cheney's comment at https://github.com/pkg/errors/issues/34#issuecomment-228231192. This package has used the `keyvalser` interface as a mechanism for extracting key/value pairs from an error. In practice this seems to work quite well, but it would be possible to write code that abuses this interface by extractng information from the error for use by the program. The thinking is that the keyvalser interface is not an obvious part of the API as documented by GoDoc, and that should help minimize abuse.