Skip to content
Merged
152 changes: 135 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,151 @@
# XERROR
# XERROR v2 (stable branch)

[![Build Status](https://api.travis-ci.org/ibrt/go-xerror.svg?branch=v2)](https://travis-ci.org/ibrt/go-xerror?branch=v2)
[![Coverage Status](https://coveralls.io/repos/github/ibrt/go-xerror/badge.svg?branch=v2)](https://coveralls.io/github/ibrt/go-xerror?branch=v2)
[![GoDoc](https://godoc.org/gopkg.in/ibrt/go-xerror.v2/xerror?status.svg)](https://godoc.org/gopkg.in/ibrt/go-xerror.v2/xerror)

```
go get gopkg.in/ibrt/go-xerror.v2/xerror
```

### Overview

Package `xerror` extends the functionality of Go's built-in `error` interface: it allows to generate nicely formatted error messages while making it easy to programmatically check for error types, and allowing to propagate additional information such as stack traces and debug values.

Features:
This package is particularly useful in applications such as API servers, where errors returned to users might contain less detail than those stored in logs. Additionally, the library interoperates well with Go's built-in errors: it allows to easily wrap them for propagation, and it generates errors that implement the `error` interface, making it suitable for use in libraries where the clients do not depend on this package.

##### Features

- a stack trace is attached to errors at creation
- additional debug values can be attached to errors for deferred out-of-band logging and reporting
- nice interface for wrapping errors and propagating them at the right level of abstraction
- easy to check for error types while generating nicely formatted messages
- compatible with Go's `error` interface
- easy to check for error types while generating nicely formatted messages, which include specifics

This package is particularly useful in applications such as API servers, where errors returned to users might contain less detail than those stored in logs. Additionally, the library interoperates well with Go's built-in errors: it allows to easily wrap them for propagation, and it generates errors that implement the `error` interface, making it suitable for use in libraries where the clients do not depend on this package.
### How To

We will now learn how to create errors, propagate them, check for error types, access stack traces and debug objects, and interoperate with the standard Go library. This how-to also attempts to describe and clarify some best practices for error handling in Go.

##### Creating a new error

To create a new error, use the `xerror.New` function:

```go
const ErrorInvalidValueForField = "invalid value for field %v"

func ValidateRequest(r *Request) error {
if r.UserID <= 0 {
return xerror.New(ErrorInvalidValueForField, "userId", request)
}
return nil
}
```

All arguments of `New` besides the format string are appended to the debug objects slice. The library counts how many placeholders are present in the format string and limits the numbers of arguments passed to `fmt.Sprintf` when generating the formatted version. Calling the Error interface methods on the newly created error would return the following:

```go
err.Error() // -> "invalid value for field userId"
err.Debug() // -> []interface{}{"userId", request}
err.Stack() // -> a slice of strings representing the stack when New is called
```

##### Propagating errors

Errors are usually propagated up the call stack as return values. It is often desirable to wrap them with information at the right level of abstraction, but the standard Go library doesn't provide a good way to do so. The `xerror.Wrap` function can be used for this purpose, as illustrated below:

```go
const (
ErrorMalformedRequestBody = "malformed request body"
ErrorBadRequest = "bad request for URL %v"
)

func ParseRequest(buf []byte) (*Request, error) {
req := &Request{}
if err := json.Unmarshal(buf, req); err != nil {
return nil, xerror.Wrap(err, ErrorMalformedRequestBody, buf)
}
return req, nil
}

func HandleRequest(r *http.Request) (*Response, error) {
buf, err := ioutil.ReadAll(r.body)
defer r.Body.Close()
if err != nil {
return nil, xerror.Wrap(err, ErrorBadRequest, r.URL, r) // first error
}
req, err := ParseRequest(buf)
if err != nil {
return nil, xerror.Wrap(err, ErrorBadRequest, r.URL, r) // second error
}

...
}
```

Calling the Error interface methods on the first error would return the following:

```go
err.Error() // -> "bad request for URL http://some-url: unexpected end of file"
err.Debug() // -> []interface{}{r.URL, r}
err.Stack() // -> a slice of strings representing the stack at Wrap call
```

Calling the Error interface methods on the second error would return the following:

```go
err.Error() // -> "bad request for URL http://some-url: malformed request body: invalid character 'b'"
err.Debug() // -> []interface{}{r.URL, r, buf}
err.Stack() // -> a slice of strings representing the stack at the first Wrap call
```

##### Determining the type of an error

This library provides functions for determining error types: `Is` and `Contains`. They exist both as top-level package functions and as methods on the `Error` interface. Error type checking in Go is usually done by storing error messages a string constants, and performing string comparisons. Unfortunately this technique doesn't work well when used together with `fmt.Errorf`, as the generated error string is not equal to the original format string. These functions instead perform the comparison on the format string, allowing to generate clearer error messages while retaining the ability to check for error types.

Let's consider the second error from the _Propagating errors_ section. Here is the result of some sample calls:

```go
err.Is(ErrorBadRequest) // -> true
err.Is(ErrorMalformedRequestBody) // -> false
err.Contains(ErrorBadRequest) // -> true
err.Contains(ErrorMalformedRequestBody) // -> true
```

In other words, `Is` only compares the format string with the outermost error in the wrap chain, while `Contains` performs the comparison on all wrapped errors. The top-level functions work similarly, but they accept any kind of `error` argument. If the given `error` is actually a `xerror.Error`, they are equivalent to calling the corresponding methods on the interface, otherwise they perform the comparison on the string version of the given error.

```go
xerror.Is(secondError, ErrorBadRequest) // -> true
xerror.Is(secondError, ErrorMalformedRequestBody) // -> false
xerror.Is(errors.New(ErrorBadRequest), ErrorBadRequest) // -> true
xerror.Contains(errors.New(ErrorBadRequest), ErrorBadRequest) // -> true
```

It is currently recommended to use the stable v1 branch, which is in maintenance mode. Refer to the `godoc` pages for usage details. More information on branches and future development follows.
##### Reporting and displaying errors

## Stable Branch (v1)
The `xerror.Error` interface extends `error`, `json.Marshaler`, and `fmt.GoStringer`. It is possible to obtain string representations of errors for various use cases:

[![Build Status](https://api.travis-ci.org/ibrt/go-xerror.svg?branch=v1)](https://travis-ci.org/ibrt/go-xerror?branch=v1)
[![Coverage Status](https://coveralls.io/repos/github/ibrt/go-xerror/badge.svg?branch=v1)](https://coveralls.io/github/ibrt/go-xerror?branch=v1)
[![GoDoc](https://godoc.org/gopkg.in/ibrt/go-xerror.v1/xerror?status.svg)](https://godoc.org/gopkg.in/ibrt/go-xerror.v1/xerror)
- calling `err.Error()` or formatting as `%s` or `%v`returns a short string
- serializing to JSON or formatting as `%#v` returns a long string

- https://github.com/ibrt/go-xerror/tree/v1
- `go get gopkg.in/ibrt/go-xerror.v1/xerror`
This is an example of short string:

## Development Branch (master):
```
bad request: malformed request body: invalid character 'b'
```

[![Build Status](https://api.travis-ci.org/ibrt/go-xerror.svg?branch=master)](https://travis-ci.org/ibrt/go-xerror?branch=master)
[![Coverage Status](https://coveralls.io/repos/github/ibrt/go-xerror/badge.svg?branch=master)](https://coveralls.io/github/ibrt/go-xerror?branch=master)
[![GoDoc](https://godoc.org/github.com/ibrt/go-xerror/xerror?status.svg)](https://godoc.org/github.com/ibrt/go-xerror/xerror)
This is an example of long string (actually on a single line):

- https://github.com/ibrt/go-xerror
- `go get github.com/ibrt/go-xerror/xerror`
```
{
"message": "bad request: malformed request body: invalid character 'b'",
"debug": [
"d2",
"d1"
],
"stack":[
"/path/to/file1.go:49 (0x8448b)",
"/path/to/file2.go:198 (0x8448b)",
...
]
}
```
127 changes: 31 additions & 96 deletions xerror/error.go
Original file line number Diff line number Diff line change
@@ -1,76 +1,39 @@
/*
Package xerror extends the functionality of Go's built-in error interface, in several ways:

- errors carry a stack trace of the location where they were first created
- it is possible to attach debug objects to errors for later reporting
- errors hold a list of messages, and can be wrapped by prepending new messages to the list
- errors can natively be treated as Go built-in errors and serialized to a string message or JSON representation

To create a new error, use the New function:

err := xerror.New("first message", "second message")

When this error is converted to string using the Error method from Go's error interface, the following is returned:

"first message: second message"

To create an augmented error given a Go error, use the Wrap function:

if _, err := pkg.Method(...); err != nil {
return xerror.Wrap(err).WithMessages("unable to execute Method")
}

If the given error is actually of type Error, the Error is immediately returned unmodified.

Errors are immutable, but modified copies of them can be obtained using WithMessages and WithDebug:

return xerror.Wrap(err).WithMessages("unable to perform action").WithDebug(ctx, req)

Error also provides methods for determining its type, by matching all messages or just the outermost one:

err := xerror.New("m2", "m1")
err.Is("m2") // true
err.Is("m1") // false
err.IsPattern(regexp.MustCompile("2$")) // true
err.IsPattern(regexp.MustCompile("1$")) // false
err.Contains("m2") // true
err.Contains("m1") // true
err.ContainsPattern(regexp.MustCompile("2$") // true
err.ContainsPattern(regexp.MustCompile("1$") // true
Package xerror extends the functionality of Go's built-in error interface: it allows to generate nicely formatted error
messages while making it easy to programmatically check for error types, and allowing to propagate additional
information such as stack traces and debug values.
*/
package xerror

import (
"encoding/json"
"fmt"
"regexp"
"strings"
)

// Error is the augmented error interface provided by this package.
type Error interface {
error
json.Marshaler
fmt.GoStringer

Is(string) bool
IsPattern(*regexp.Regexp) bool
Contains(string) bool
ContainsPattern(*regexp.Regexp) bool
Debug() []interface{}
Stack() []string
Clone() Error
}

// xerror is the internal implementation of Error
type xerror struct {
type xerr struct {
msg string
fmts []string
dbg []interface{}
stack []string
}

// xerrorJSON is used to serialize Error to JSON
type xerrorJSON struct {
type xerrJSON struct {
Message string `json:"message"`
Debug []interface{} `json:"debug,omitempty"`
Stack []string `json:"stack"`
Expand All @@ -79,7 +42,7 @@ type xerrorJSON struct {
// New returns a new augmented error. Parameters that don't have a placeholder in the format string are only stored as debug objects.
func New(format string, v ...interface{}) Error {
v = nilToEmpty(v)
return &xerror{
return &xerr{
msg: safeSprintf(format, v),
fmts: []string{format},
dbg: v,
Expand All @@ -98,31 +61,35 @@ func Wrap(err error, format string, v ...interface{}) Error {
}

// Error implements the `error` interface.
func (e *xerror) Error() string {
func (e *xerr) Error() string {
return e.msg
}

// MarshalJSON implements the `json.Marshaler` interface.
func (e *xerror) MarshalJSON() ([]byte, error) {
return json.Marshal(&xerrorJSON{
func (e *xerr) MarshalJSON() ([]byte, error) {
return json.Marshal(&xerrJSON{
Message: e.msg,
Debug: e.dbg,
Stack: e.stack,
})
}

// Is returns true if the outermost error message format equals the given message format, false otherwise.
func (e *xerror) Is(fmt string) bool {
return e.fmts[0] == fmt
// GoString implements the `fmt.GoStringer` interface.
func (e *xerr) GoString() string {
buf, err := e.MarshalJSON()
if err != nil {
return fmt.Sprintf("!ERROR(%v)", err)
}
return string(buf)
}

// IsPattern returns true if the outermost error message format matches the given pattern, false otherwise.
func (e *xerror) IsPattern(pattern *regexp.Regexp) bool {
return pattern.MatchString(e.fmts[0])
// Is returns true if the outermost error message format equals the given message format, false otherwise.
func (e *xerr) Is(fmt string) bool {
return e.fmts[0] == fmt
}

// Contains returns true if the error contains the given message format, false otherwise.
func (e *xerror) Contains(format string) bool {
func (e *xerr) Contains(format string) bool {
for _, f := range e.fmts {
if f == format {
return true
Expand All @@ -131,29 +98,19 @@ func (e *xerror) Contains(format string) bool {
return false
}

// ContainsPattern returns true if the error contains a message format that matches the given pattern, false otherwise.
func (e *xerror) ContainsPattern(pattern *regexp.Regexp) bool {
for _, f := range e.fmts {
if pattern.MatchString(f) {
return true
}
}
return false
}

// Debug returns the slice of debug objects.
func (e *xerror) Debug() []interface{} {
func (e *xerr) Debug() []interface{} {
return e.dbg
}

// Stack returns the stack trace associated with the error.
func (e *xerror) Stack() []string {
func (e *xerr) Stack() []string {
return e.stack
}

// Clone returns an exact copy of the `Error`.
func (e *xerror) Clone() Error {
return &xerror{
func (e *xerr) Clone() Error {
return &xerr{
msg: e.msg,
fmts: append(make([]string, 0, len(e.fmts)), e.fmts...),
dbg: append(make([]interface{}, 0, len(e.dbg)), e.dbg...),
Expand All @@ -166,51 +123,29 @@ func Is(err error, format string) bool {
if err == nil {
return false
}
if xerr, ok := err.(*xerror); ok {
if xerr, ok := err.(*xerr); ok {
return xerr.Is(format)
}
return err.Error() == format
}

// IsPattern is like Is but uses regexp matching rather than string comparison.
func IsPattern(err error, pattern *regexp.Regexp) bool {
if err == nil {
return false
}
if xerr, ok := err.(*xerror); ok {
return xerr.IsPattern(pattern)
}
return pattern.MatchString(err.Error())
}

// Contains is like Is, but in case `err` is of type `Error` compares the message format with all attached message formats.
func Contains(err error, format string) bool {
if err == nil {
return false
}
if xerr, ok := err.(*xerror); ok {
if xerr, ok := err.(*xerr); ok {
return xerr.Contains(format)
}
return err.Error() == format
}

// ContainsPattern is like Contains but uses regexp matching rather than string comparison.
func ContainsPattern(err error, pattern *regexp.Regexp) bool {
if err == nil {
return false
}
if xerr, ok := err.(*xerror); ok {
return xerr.ContainsPattern(pattern)
}
return pattern.MatchString(err.Error())
}

// cloneOrNew wraps the given `error` unless it is already of type `*xerror`, in which case it returns a copy
func cloneOrNew(err error) *xerror {
if xerr, ok := err.(*xerror); ok {
return xerr.Clone().(*xerror)
func cloneOrNew(err error) *xerr {
if x, ok := err.(*xerr); ok {
return x.Clone().(*xerr)
}
return New(err.Error()).(*xerror)
return New(err.Error()).(*xerr)
}

// safeSprintf is like `fmt.Sprintf`, but passes through only at most parameters as placeholders in the format string
Expand Down
Loading