Skip to content
This repository has been archived by the owner on Aug 31, 2019. It is now read-only.
/ errors Public archive

Simple error handling primitives that work well with structured logging

License

Notifications You must be signed in to change notification settings

jjeffery/errors

Repository files navigation

errors GoDoc License Build Status Coverage Status GoReportCard

Package errors provides simple error handling primitives that work well with structured logging.

This package is deprecated. Use the kv package instead. It provides support for creating errors with key/value pairs. The idea of having a drop-in replacement for the standard library errors package was based on the fact that errors only exported one simple function. The Go 2 draft proposes additional functions and types for the standard library errors package, and any attempts to use the new standard library package with this package will be annoying.

Acknowledgement

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.

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:

Operation Description
New create a new error
Wrap wrap an existing error with an optional message
With attach key/value pairs to an error

New — create a new 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")

Wrap — add a message to an error

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

With — add key/value pairs to an error

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

(Dave Cheney has written up some good reasons to avoid a fluent API. Experience will show if this presents a problem, but to date it has felt like it leads to simpler, more readable code).

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:

// 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.

Read the package documentation for more information.

Licence

MIT