errors

package module
v1.0.3 Latest Latest
Warning

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

Go to latest
Published: Mar 1, 2019 License: MIT Imports: 3 Imported by: 17

README

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

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

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.

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

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

func New(message string) Error

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

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

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.

Jump to

Keyboard shortcuts

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