errors: github.com/jjeffery/errors Index | Examples | Files

package errors

import "github.com/jjeffery/errors"

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.

Code:

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

Package Files

cause.go context.go doc.go error.go errors.go

func Cause Uses

func Cause(err error) error

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.

Code:

// 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())
}

type Context Uses

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 Uses

func With(keyvals ...interface{}) Context

With creates a context with the key/value pairs.

Code:

// ... 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"))
}

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 Uses

type Error interface {
    Error() string
    With(keyvals ...interface{}) Error
}

The Error interface implements the builtin error interface, and implements an additional method that attaches key value pairs to the error.

func New Uses

func New(message string) Error

New returns a new error with a given message.

Code:

name := getNameOfThing()

if !isValidName(name) {
    fmt.Println(errors.New("invalid name").With("name", name))
}

Output:

invalid name name="!not-valid"

func Wrap Uses

func Wrap(err error, message ...string) Error

Wrap creates an error that wraps an existing error. If err is nil, Wrap returns nil.

Code:

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

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.

Package errors imports 3 packages (graph) and is imported by 16 packages. Updated 2018-04-01. Refresh now. Tools for package owners.