errors

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Aug 17, 2018 License: BSD-3-Clause Imports: 7 Imported by: 25

README

errors

BSD-2-Clause Beta Build status Coverage status Go Report Card Github issues Github pull requests GoDoc

Go errors is inspired by pkg/errors and uses a similar API but adds support for error codes. The default error code 0 is ignored.

import (
	errs "github.com/bdlm/errors"
)

One of the most common frustrations with go error handling is the lack of exceptions in the language. You can use the panic/recover method to simulate the behavior but that's akward, clunky, and hard to follow for many people. And for me, I commonly handle an exception and run additional code or may simply use an exception to inform my code about internal behavior from deeper in the call stack. panic/recover makes that impossible without what always feels like spaghetti code.

As Pike says, errors are values, and when panic/recover isn't a reasonable solution you have to handle passing that information up the stack yourself. Which kind of sucks and leaves us with the if err != nil idiom which is fairly useless without a solid pattern behind it.

Since the idom is that we handle the error all the way up the stack anyway, it's trivial to make errors much, much more useful with a good error package. pkg/errors makes this very easy and supports tracing the call stack and the error callers, but it still doesn't have the concept of typed exceptions. If I get an EOF error I may want to do something different than if I get a FileNotFound, and even with pkg/errors you have to inspect the contents of the error message. Error messages are for people, not programs.

It is possible to define custom errors for each type but that's verbose and still very inflexible. This package mimics pkg/errors but makes the error type a first class value. The default type (0) behaves the same as pkg/errors, but several common types are included and custom error codes are fully supported. Custom error types are fully compatible with this package as well and can be used freely with error type codes.

In addition to typing errors, several optional convenience properties are available, including internal and external error messages (meant to separate internal error or debugging information from user-friendly messages), and HTTP status codes that can help facilitate API responses.

Error stacks

An error stack is an array of errors.

Create a new stack
if !decodeSomeJSON() {
    err := errs.New(0, "validation failed")
}
Base a new stack off any error
err := decodeSomeJSON()
err = errs.Wrap(err, 0, "could not read configuration")

Error codes

Adding support for error codes is the primary motivation behind this project. See codes.go. HTTP is optional and a convenience property that allows automation of HTTP status responses based on internal error codes. The Code definition associated with error at the top of the stack (most recent error) should be used for HTTP status output.

import (
	"net/http"

	errs "github.com/bdlm/errors"
)

const (
	// Error codes below 1000 are reserved future use by the
	// "github.com/bdlm/errors" package.
	UserError errs.Code = iota + 1000
	SaveError
)

func init() {
	errs.Codes[UserError] = errs.Metadata{
		Int:  "bad user input",
		Ext:  "A user error occurred",
		HTTP: http.StatusBadRequest,
	}
	errs.Codes[SaveError] = errs.Metadata{
		Int:  "could not save data",
		Ext:  "An internal server occurred",
		HTTP: http.StatusInternalServerError,
	}
}

func SaveData() error {

	...

	if couldntWriteData {
		return errs.New(SaveError, "SaveData could not write the data")
	}
	return nil
}

func handler(w http.ResponseWriter, r *http.Request) {
	err := SaveData()
	if nil != err {
		w.WriteHeader(err.(errs.Err).HTTPStatus()) // 500 status
	}
}

Define a new error with an error code

Creating a new error defines the root of a backtrace.

_, err := ioutil.ReadAll(r)
if err != nil {
	return errs.New(errs.ErrUnknown, "read failed")
}

Adding context to an error

The errors.Wrap function returns a new error stack, adding context as the top error in the stack:

_, err := ioutil.ReadAll(r)
if err != nil {
	return errs.Wrap(err, errs.ErrUnknown, "read failed")
}

In this case, if the original err is not an instance of Err, that error becomes the root of the error stack.

Building an error stack

Most cases will build a stack trace off a series of errors returned from the call stack:

import (
	"fmt"

	errs "github.com/bdlm/errors"
)

const (
	// Error codes below 1000 are reserved future use by the
	// "github.com/bdlm/errors" package.
	ConfigurationNotValid errs.Code = iota + 1000
)

func loadConfig() error {
	err := decodeConfig()
	return errs.Wrap(err, ConfigurationNotValid, "service configuration could not be loaded")
}

func decodeConfig() error {
	err := readConfig()
	return errs.Wrap(err, errs.ErrInvalidJSON, "could not decode configuration data")
}

func readConfig() error {
	err := fmt.Errorf("read: end of input")
	return errs.Wrap(err, errs.ErrEOF, "could not read configuration file")
}

func someWork() error {
	return fmt.Errorf("failed to do work")
}

But for cases where a set of errors need to be captured from a single procedure, the With() call can be used. The with call adds an error to the stack behind the leading error:

import (
	errs "github.com/bdlm/errors"
)

func doSteps() error {
	var errStack errs.Err

	err := doStep1()
	if nil != err {
		errStack.With(err, "step 1 failed")
	}


	err = doStep2()
	if nil != err {
		errStack.With(err, "step 2 failed")
	}

	err = doStep3()
	if nil != err {
		errStack.With(err, "step 3 failed")
	}

	return errStack
}

Root cause of an error stack

Retrieving the root cause of an error stack is straightforward:

log.Println(err.(errs.Stack).Cause())

You can easily switch on the type of any error in the stack (including the causer) as usual:

switch err.(errs.Err).Cause().(type) {
case *MyError:
        // handle specifically
default:
        // unknown error
}

Iterating the error stack

Becase an error stack is just an array of errors iterating through it is trivial:

for _, e := range err.(errs.Err) {
	fmt.Println(e.Code())
	fmt.Println(e.Error())
	fmt.Println(e.Msg())  // In the case of Wrap(), it is possible to suppliment
	                      // an error with additional information, which is
	                      // returned by Msg(). Otherwise, Msg() returns the same
	                      // string as Error().
}

Output formats

The Formatter interface has been implemented to provide access to a stack trace with the %v verb.

Standard error output, use with error codes to ensure appropriate user-facing messages %v:

0000: failed to load configuration

Single-line stack trace, useful for logging %-v:

#4 - "failed to load configuration" examples_test.go:36 `github.com/bdlm/errors_test.ExampleWrap_backtrace` {0000: unknown error} #3 - "service configuration could not be loaded" mocks_test.go:16 `github.com/bdlm/errors_test.loadConfig` {0001: fatal error} #2 - "could not decode configuration data" mocks_test.go:21 `github.com/bdlm/errors_test.decodeConfig` {0200: invalid JSON data could not be decoded} #1 - "could not read configuration file" mocks_test.go:26 `github.com/bdlm/errors_test.readConfig` {0100: unexpected EOF} #0 - "read: end of input" mocks_test.go:26 `github.com/bdlm/errors_test.readConfig` {0000: unknown error}

Multi-line condensed stack trace %#v:

#4 - "failed to load configuration" examples_test.go:36 `github.com/bdlm/errors_test.ExampleWrap_backtrace` {0000: unknown error}
#3 - "service configuration could not be loaded" mocks_test.go:16 `github.com/bdlm/errors_test.loadConfig` {0001: fatal error}
#2 - "could not decode configuration data" mocks_test.go:21 `github.com/bdlm/errors_test.decodeConfig` {0200: invalid JSON data could not be decoded}
#1 - "could not read configuration file" mocks_test.go:26 `github.com/bdlm/errors_test.readConfig` {0100: unexpected EOF}
#0 - "read: end of input" mocks_test.go:26 `github.com/bdlm/errors_test.readConfig` {0000: unknown error}

Multi-line detailed stack trace %+v:

#4: `github.com/bdlm/errors_test.ExampleWrap_backtrace`
	error:   failed to load configuration
	line:    examples_test.go:36
	code:    0000: unknown error
	message: 0000: failed to load configuration
#3: `github.com/bdlm/errors_test.loadConfig`
	error:   service configuration could not be loaded
	line:    mocks_test.go:16
	code:    0001: fatal error
	message: 0001: Internal Server Error
#2: `github.com/bdlm/errors_test.decodeConfig`
	error:   could not decode configuration data
	line:    mocks_test.go:21
	code:    0200: invalid JSON data could not be decoded
	message: 0200: Invalid JSON Data
#1: `github.com/bdlm/errors_test.readConfig`
	error:   could not read configuration file
	line:    mocks_test.go:26
	code:    0100: unexpected EOF
	message: 0100: End of input
#0: `github.com/bdlm/errors_test.readConfig`
	error:   read: end of input
	line:    mocks_test.go:26
	code:    0000: unknown error
	message: 0000: read: end of input

Documentation

Overview

Package errors is inspired by `pkg/errors` (https://github.com/pkg/errors) and uses a similar API but adds support for error codes. Error codes are always optional.

import (
	errs "github.com/mkenney/go-errors"
)

Error stacks

An error stack is an array of errors.

Create a new stack

if !decodeSomeJSON() {
	err := errs.New("validation failed")
}

Base a new stack off any error

err := decodeSomeJSON()
err = errs.Wrap(err, "could not read configuration")

Define error codes

Adding support for error codes is the primary motivation behind this project. See `codes.go` (https://github.com/mkenney/go-errors/blob/master/codes.go). `HTTPStatus` is optional and a convenience property that allows automation of HTTP status responses based on internal error codes. The `Code` definition associated with error at the top of the stack (most recent error) should be used for HTTP status output.

import (
	errs "github.com/mkenney/go-errors"
)

const (
	// Error codes below 1000 are reserved future use by the errors
	// package.
	UserError errs.Code = iota + 1000
	InternalError
)

func init() {
	errs.Codes[UserError] = errs.Metadata{
		Internal:   "bad user input",
		External:   "A user error occurred",
		HTTPStatus: 400,
	}
	errs.Codes[InternalError] = errs.Metadata{
		Internal:   "could not save data",
		External:   "An internal server occurred",
		HTTPStatus: 500,
	}
}

func SomeFunc() error {
	return errs.New("SomeFunc failed because of things", InternalError)
}

Define a new error with an error code

Creating a new error defines the root of a backtrace.

_, err := ioutil.ReadAll(r)
if err != nil {
	return errs.New("read failed", errs.ErrUnknown)
}

Adding context to an error

The errors.Wrap function returns a new error that adds context to the original error and starts an error stack:

_, err := ioutil.ReadAll(r)
if err != nil {
	return errs.Wrap(err, "read failed", errs.ErrUnknown)
}

In this case, if the original `err` is not an instance of `Stack`, that error becomes the root of the error stack.

Building an error stack

Most cases will build a stack trace off a series of errors returned from the call stack:

import (
	"fmt"
	errs "github.com/mkenney/go-errors"
)

func main() {
	err := loadConfig()
	fmt.Printf("%#v", err)
}

func readConfig() error {
	err := fmt.Errorf("read: end of input")
	return errs.Wrap(err, "could not read configuration file", errs.ErrEOF)
}

func decodeConfig() error {
	err := readConfig()
	return errs.Wrap(err, "could not decode configuration data", errs.ErrInvalidJSON)
}

func loadConfig() error {
	err := decodeConfig()
	return errs.Wrap(err, "service configuration could not be loaded", errs.ErrFatal)
}

But for cases where a set of errors need to be captured from a single procedure, the `With()` call can be used. The with call adds an error to the stack behind the leading error:

import (
	errs "github.com/mkenney/go-errors"
)

func doSteps() error {
	var errStack errs.Err

	err := doStep1()
	if nil != err {
		errStack.With(err, "step 1 failed")
	}

	err = doStep2()
	if nil != err {
		errStack.With(err, "step 2 failed")
	}

	err = doStep3()
	if nil != err {
		errStack.With(err, "step 3 failed")
	}

	return errStack
}

Root cause of an error stack

Retrieving the root cause of an error stack is straightforward:

log.Println(err.(errs.Stack).Cause())

Similar to `pkg/errors`, you can easily switch on the type of any error in the stack (including the causer):

switch err.(errs.Err).Cause().(type) {
case *MyError:
		// handle specifically
default:
		// unknown error
}

Output formats

The Formatter interface has been implemented to provide access to a stack trace with the `%v` verb.

Standard error output, use with error codes to ensure appropriate user-facing messages `%s`:

0002: Internal Server Error

Single-line stack trace, useful for logging `%v`:

#0 - "service configuration could not be loaded" example_test.go:22 `github.com/mkenney/go-errors_test.loadConfig` {0002: a fatal error occurred} #1 - "could not decode configuration data" example_test.go:17 `github.com/mkenney/go-errors_test.decodeConfig` {0200: invalid JSON data could not be decoded} #2 - "could not read configuration file" example_test.go:12 `github.com/mkenney/go-errors_test.readConfig` {0100: unexpected EOF}

Multi-line condensed stack trace `%#v`:

#0 - "service configuration could not be loaded" example_test.go:22 `github.com/mkenney/go-errors_test.loadConfig` {0002: a fatal error occurred}
#1 - "could not decode configuration data" example_test.go:17 `github.com/mkenney/go-errors_test.decodeConfig` {0200: invalid JSON data could not be decoded}
#2 - "could not read configuration file" example_test.go:12 `github.com/mkenney/go-errors_test.readConfig` {0100: unexpected EOF}

Multi-line detailed stack trace `%+v`:

#0: `github.com/mkenney/go-errors_test.loadConfig`
	error:   service configuration could not be loaded
	line:    example_test.go:22
	code:    2 - a fatal error occurred
	entry:   17741072
	message: Internal Server Error

#1: `github.com/mkenney/go-errors_test.decodeConfig`
	error:   could not decode configuration data
	line:    example_test.go:17
	code:    200 - invalid JSON data could not be decoded
	entry:   17740848
	message: Invalid JSON Data

#2: `github.com/mkenney/go-errors_test.readConfig`
	error:   could not read configuration file
	line:    example_test.go:12
	code:    100 - unexpected EOF
	entry:   17740576
	message: End of input

Index

Examples

Constants

View Source
const (
	// ErrUnknown - 0: An unknown error occurred.
	ErrUnknown std.Code = iota
	// ErrFatal - 1: An fatal error occurred.
	ErrFatal
)

Internal errors

View Source
const (
	// ErrEOF - 100: An invalid HTTP method was requested.
	ErrEOF std.Code = iota + 100
	ErrReader
)

I/O errors

View Source
const (
	// ErrDecodingFailed - Decoding failed due to an error with the data.
	ErrDecodingFailed std.Code = iota + 200
	// ErrDecodingJSON - JSON data could not be decoded.
	ErrDecodingJSON
	// ErrDecodingToml - Toml data could not be decoded.
	ErrDecodingToml
	// ErrDecodingYaml - Yaml data could not be decoded.
	ErrDecodingYaml
	// ErrEncodingFailed - Encoding failed due to an error with the data.
	ErrEncodingFailed
	// ErrEncodingJSON - JSON data could not be encoded.
	ErrEncodingJSON
	// ErrEncodingToml - Toml data could not be encoded.
	ErrEncodingToml
	// ErrEncodingYaml - Yaml data could not be encoded.
	ErrEncodingYaml
	// ErrInvalidJSON - Data is not valid JSON.
	ErrInvalidJSON
	// ErrInvalidToml - Data is not valid Toml.
	ErrInvalidToml
	// ErrInvalidYaml - Data is not valid Yaml.
	ErrInvalidYaml
	// ErrTypeConversionFailed - Data type conversion failed.
	ErrTypeConversionFailed
)

Encoding errors

View Source
const (
	// ErrInvalidHTTPMethod - 300: An invalid HTTP method was requested.
	ErrInvalidHTTPMethod std.Code = iota + 300
)

Server errors

Variables

View Source
var Codes = map[std.Code]Coder{}

Codes contains a map of error codes to metadata

Functions

This section is empty.

Types

type Call

type Call struct {
	// contains filtered or unexported fields
}

Call implements bdlm/std/error.Caller, holding runtime.Caller data.

func (Call) File

func (call Call) File() string

File implements bdlm/std/error.Caller, returning the caller file name.

func (Call) Line

func (call Call) Line() int

Line implements bdlm/std/error.Caller, returning the caller line number.

func (Call) Ok

func (call Call) Ok() bool

Ok implements bdlm/std/error.Caller, returning whether the caller data was successfully recovered.

func (Call) Pc

func (call Call) Pc() uintptr

Pc implements bdlm/std/error.Caller, returning the caller program counter.

func (Call) String

func (call Call) String() string

String implements the Stringer interface

type Code

type Code int

Code defines an error code type.

type Coder

type Coder interface {
	// Internal only (logs) error text.
	Detail() string
	// HTTP status that should be used for the associated error code.
	HTTPStatus() int
	// External (user) facing error text.
	String() string
}

Coder defines an interface for an error code.

type Err

type Err []ErrMsg

Err defines an error heap.

func From

func From(code std.Code, err error) Err

From creates a new error stack based on a provided error and returns it.

Example
package main

import (
	"errors"
	"fmt"

	errs "github.com/bdlm/errors"
)

func main() {
	// Converting an error from another package into an error stack is
	// straightforward.
	err := errors.New("my error")
	if _, ok := err.(errs.Err); !ok {
		err = errs.From(0, err)
	}

	fmt.Println(err)
	fmt.Println(err.(errs.Err).Detail())

}
Output:

0000: An unknown error occurred
my error

func New

func New(code std.Code, msg string, data ...interface{}) Err

New returns an error with caller information for debugging.

Example
package main

import (
	"fmt"

	errs "github.com/bdlm/errors"
)

func main() {
	var err error

	// If an error code isn't used or doesn't have a corresponding
	// ErrCode defined, the error message is returned.
	err = errs.New(0, "this is an error message")
	fmt.Println(err)

	// If an error with a corresponding ErrCode is specified, the
	// user-safe error string mapped to the error code is returned,
	// along with the code.
	err = errs.New(errs.ErrFatal, "this is an error message")
	fmt.Println(err)

}
Output:

0000: An unknown error occurred
0001: A fatal error occurred

func Wrap

func Wrap(err error, code std.Code, msg string, data ...interface{}) Err

Wrap wraps an error into a new stack led by msg.

Example (Backtrace)
// To build up an error stack, add context to each error before
// returning it up the call stack.
err := loadConfig()
if nil != err {
	err = errs.Wrap(err, 0, "failed to load configuration")
}

// The %v formatting verb can be used to print out the stack trace
// in various ways. The %v verb is the default and prints out the
// standard error message.
fmt.Println(err)

// The %-v verb is useful for logging and prints the trace on a
// single line.
fmt.Printf("%-v\n\n", err)

// The %#v verb prints each cause in the stack on a separate line.
fmt.Printf("%#v\n\n", err)

// The %+v verb prints a verbose detailed backtrace intended for
// human consumption.
fmt.Printf("%+v\n\n", err)
Output:

0000: An unknown error occurred
#4 - "failed to load configuration" examples_test.go:37 `github.com/bdlm/errors_test.ExampleWrap_backtrace` {0000: failed to load configuration} #3 - "service configuration could not be loaded" mocks_test.go:31 `github.com/bdlm/errors_test.loadConfig` {1000: the configuration is invalid} #2 - "could not decode configuration data" mocks_test.go:36 `github.com/bdlm/errors_test.decodeConfig` {0208: could not decode configuration data} #1 - "could not read configuration file" mocks_test.go:41 `github.com/bdlm/errors_test.readConfig` {0100: unexpected EOF} #0 - "read: end of input" mocks_test.go:41 `github.com/bdlm/errors_test.readConfig` {0000: read: end of input}

#4 - "failed to load configuration" examples_test.go:37 `github.com/bdlm/errors_test.ExampleWrap_backtrace` {0000: failed to load configuration}
#3 - "service configuration could not be loaded" mocks_test.go:31 `github.com/bdlm/errors_test.loadConfig` {1000: the configuration is invalid}
#2 - "could not decode configuration data" mocks_test.go:36 `github.com/bdlm/errors_test.decodeConfig` {0208: could not decode configuration data}
#1 - "could not read configuration file" mocks_test.go:41 `github.com/bdlm/errors_test.readConfig` {0100: unexpected EOF}
#0 - "read: end of input" mocks_test.go:41 `github.com/bdlm/errors_test.readConfig` {0000: read: end of input}

#4: `github.com/bdlm/errors_test.ExampleWrap_backtrace`
	error:   failed to load configuration
	line:    examples_test.go:37
	code:    0000: failed to load configuration
	message: 0000: An unknown error occurred
#3: `github.com/bdlm/errors_test.loadConfig`
	error:   service configuration could not be loaded
	line:    mocks_test.go:31
	code:    1000: the configuration is invalid
	message: 1000: Configuration not valid
#2: `github.com/bdlm/errors_test.decodeConfig`
	error:   could not decode configuration data
	line:    mocks_test.go:36
	code:    0208: could not decode configuration data
	message: 0208: could not decode configuration data
#1: `github.com/bdlm/errors_test.readConfig`
	error:   could not read configuration file
	line:    mocks_test.go:41
	code:    0100: unexpected EOF
	message: 0100: End of input
#0: `github.com/bdlm/errors_test.readConfig`
	error:   read: end of input
	line:    mocks_test.go:41
	code:    0000: read: end of input
	message: 0000: An unknown error occurred

func (Err) Caller added in v0.1.1

func (errs Err) Caller() std.Caller

Caller returns the most recent error caller.

func (Err) Cause

func (errs Err) Cause() error

Cause returns the root cause of an error stack.

func (Err) Code

func (errs Err) Code() std.Code

Code returns the most recent error code.

func (Err) Detail

func (errs Err) Detail() string

Detail implements the Coder interface. Detail returns the single-line stack trace.

func (Err) Error

func (errs Err) Error() string

Error implements the error interface.

func (Err) Format

func (errs Err) Format(state fmt.State, verb rune)

Format implements fmt.Formatter. https://golang.org/pkg/fmt/#hdr-Printing

Format formats the stack trace output. Several verbs are supported:

%s  - Returns the user-safe error string mapped to the error code or
    the error message if none is specified.

%v  - Alias for %s

%#v - Returns the full stack trace in a single line, useful for
	logging. Same as %#v with the newlines escaped.

%-v - Returns a multi-line stack trace, one column-delimited line
    per error.

%+v - Returns a multi-line detailed stack trace with multiple lines
      per error. Only useful for human consumption.

func (Err) HTTPStatus

func (errs Err) HTTPStatus() int

HTTPStatus returns the associated HTTP status code, if any. Otherwise, returns 200.

func (Err) Msg

func (errs Err) Msg() string

Msg returns the error message.

func (Err) String

func (errs Err) String() string

String implements the stringer and Coder interfaces.

func (Err) Trace added in v0.1.1

func (errs Err) Trace() std.Trace

Trace returns the call stack.

func (Err) With

func (errs Err) With(err error, msg string, data ...interface{}) Err

With adds a new error to the stack without changing the leading cause.

Example
// To add to an error stack without modifying the leading cause, add
// additional errors to the stack with the With() method.
err := loadConfig()
if nil != err {
	if e, ok := err.(errs.Err); nil != err && ok {
		err = e.With(errs.New(0, "failed to load configuration"), "loadConfig returned an error")
	} else {
		err = errs.From(0, err)
	}
}

fmt.Println(err)
Output:

1000: Configuration not valid

type ErrCode

type ErrCode struct {
	// External (user) facing error text.
	Ext string
	// Internal only (logs) error text.
	Int string
	// HTTP status that should be used for the associated error code.
	HTTP int
}

ErrCode implements coder

func (ErrCode) Detail

func (code ErrCode) Detail() string

Detail returns the internal error message, if any.

func (ErrCode) HTTPStatus

func (code ErrCode) HTTPStatus() int

HTTPStatus returns the associated HTTP status code, if any. Otherwise, returns 200.

func (ErrCode) String

func (code ErrCode) String() string

String implements stringer. String returns the external error message, if any.

type ErrMsg

type ErrMsg interface {
	Caller() std.Caller
	Code() std.Code
	Error() string
	Msg() string
	SetCode(std.Code) ErrMsg
	Trace() std.Trace
}

ErrMsg defines the interface to error message data.

type Msg

type Msg struct {
	// contains filtered or unexported fields
}

Msg defines a single error message.

func (Msg) Caller

func (msg Msg) Caller() std.Caller

Caller implements ErrMsg.

func (Msg) Code

func (msg Msg) Code() std.Code

Code implements ErrMsg.

func (Msg) Error

func (msg Msg) Error() string

Error implements error.

func (Msg) Msg

func (msg Msg) Msg() string

Msg implements ErrMsg.

func (Msg) SetCode

func (msg Msg) SetCode(code std.Code) ErrMsg

SetCode implements ErrMsg.

func (Msg) String

func (msg Msg) String() string

String implements Stringer.

func (Msg) Trace added in v0.1.1

func (msg Msg) Trace() std.Trace

Trace implements ErrMsg.

Jump to

Keyboard shortcuts

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