From 2df0c564e04cedfbf1fda7552a18bc2fad36b137 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 15:29:40 -0700 Subject: [PATCH 01/17] Update README.md --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b350c9d..63d4b62 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -# 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) 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. @@ -16,9 +20,7 @@ It is currently recommended to use the stable v1 branch, which is in maintenance ## Stable Branch (v1) -[![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) + - https://github.com/ibrt/go-xerror/tree/v1 - `go get gopkg.in/ibrt/go-xerror.v1/xerror` From 4cdf8bed243b09f6a379f044e5ae84acecdd98ee Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 15:31:14 -0700 Subject: [PATCH 02/17] Update README.md --- README.md | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 63d4b62..2226533 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ -# XERROR (v2 - stable branch) +# 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 +``` + 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: @@ -17,19 +21,3 @@ 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. 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. - -## Stable Branch (v1) - - - -- https://github.com/ibrt/go-xerror/tree/v1 -- `go get gopkg.in/ibrt/go-xerror.v1/xerror` - -## Development Branch (master): - -[![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) - -- https://github.com/ibrt/go-xerror -- `go get github.com/ibrt/go-xerror/xerror` From 8732a6a24aa9c146155a890deea46a26cff2bb4a Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 15:52:58 -0700 Subject: [PATCH 03/17] Update README.md --- README.md | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2226533..b362178 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,13 @@ 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 @@ -18,6 +22,28 @@ Features: - 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. +##### Rationale + + -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. +### 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 attempts to describe and clarify the best practices for error handling in Go. + +##### Creating a new error + +```go +// Defining each error type as a constant string is a good practice. +const ErrorInvalidValueForField = "invalid value for field %v" + +// The return type of this function could alternatively be xerror.Error. +// Since it's public and possibly part of a library, we return error. +func ValidateRequest(r *Request) error { + if r.UserID == "" { + // "userId" replaces the %v placeholder in the error string + // extra arguments such as `request` are instead only attached to the debug objects slice + return xerror.New(ErrorInvalidValueForField, "userId", request) + } + return nil +} +``` From 5c13184f4948a72e845a2c73e17dc1423d6c6c82 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 16:22:54 -0700 Subject: [PATCH 04/17] Update README.md --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b362178..a4ef170 100644 --- a/README.md +++ b/README.md @@ -22,16 +22,14 @@ This package is particularly useful in applications such as API servers, where e - compatible with Go's `error` interface - easy to check for error types while generating nicely formatted messages, which include specifics -##### Rationale - - - ### 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 attempts to describe and clarify the best practices for error handling in Go. ##### Creating a new error +- https://godoc.org/gopkg.in/ibrt/go-xerror.v2/xerror#New + ```go // Defining each error type as a constant string is a good practice. const ErrorInvalidValueForField = "invalid value for field %v" From 3ded987db48d714c38caa8667f34657179e1ba06 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 16:28:03 -0700 Subject: [PATCH 05/17] Remove IsPattern and ContainsPattern. Rename xerror struct to xerr. --- xerror/error.go | 117 ++++++++----------------------------------- xerror/error_test.go | 60 +--------------------- 2 files changed, 22 insertions(+), 155 deletions(-) diff --git a/xerror/error.go b/xerror/error.go index 629b6f5..4957d99 100644 --- a/xerror/error.go +++ b/xerror/error.go @@ -1,49 +1,13 @@ /* -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" ) @@ -53,16 +17,14 @@ type Error interface { json.Marshaler 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{} @@ -70,7 +32,7 @@ type xerror struct { } // 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"` @@ -79,7 +41,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, @@ -98,13 +60,13 @@ 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, @@ -112,17 +74,12 @@ func (e *xerror) MarshalJSON() ([]byte, error) { } // Is returns true if the outermost error message format equals the given message format, false otherwise. -func (e *xerror) Is(fmt string) bool { +func (e *xerr) Is(fmt string) bool { return e.fmts[0] == fmt } -// 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]) -} - // 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 @@ -131,29 +88,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...), @@ -166,51 +113,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 diff --git a/xerror/error_test.go b/xerror/error_test.go index a9784c2..c27ba28 100644 --- a/xerror/error_test.go +++ b/xerror/error_test.go @@ -5,7 +5,6 @@ import ( "errors" "github.com/ibrt/go-xerror/xerror" "github.com/stretchr/testify/assert" - "regexp" "testing" ) @@ -139,34 +138,6 @@ func TestIsTopLevelError(t *testing.T) { assert.False(t, xerror.Is(err, "fmt p1")) } -func TestIsPattern_Method(t *testing.T) { - err := xerror.Wrap(xerror.New("fmt %v x", "p1"), "fmt2 %v y", "p2") - assert.Equal(t, "fmt2 p2 y: fmt p1 x", err.Error()) - assert.True(t, err.IsPattern(regexp.MustCompile("%v y$"))) - assert.False(t, err.IsPattern(regexp.MustCompile("p2 y$"))) - assert.False(t, err.IsPattern(regexp.MustCompile("%v x$"))) - assert.False(t, err.IsPattern(regexp.MustCompile("p1 x$"))) -} - -func TestIsPattern_TopLevelNilErr(t *testing.T) { - assert.False(t, xerror.IsPattern(nil, regexp.MustCompile("r"))) -} - -func TestIsPattern_TopLevelNativeErr(t *testing.T) { - err := errors.New("msg") - assert.True(t, xerror.IsPattern(err, regexp.MustCompile("^m"))) - assert.False(t, xerror.IsPattern(err, regexp.MustCompile("^e"))) -} - -func TestIsPattern_TopLevelError(t *testing.T) { - err := xerror.Wrap(xerror.New("fmt %v x", "p1"), "fmt2 %v y", "p2") - assert.Equal(t, "fmt2 p2 y: fmt p1 x", err.Error()) - assert.True(t, xerror.IsPattern(err, regexp.MustCompile("%v y$"))) - assert.False(t, xerror.IsPattern(err, regexp.MustCompile("p2 y$"))) - assert.False(t, xerror.IsPattern(err, regexp.MustCompile("%v x$"))) - assert.False(t, xerror.IsPattern(err, regexp.MustCompile("p1 x$"))) -} - func TestContains_Method(t *testing.T) { err := xerror.Wrap(xerror.New("fmt %v", "p1"), "fmt2 %v", "p2") assert.Equal(t, "fmt2 p2: fmt p1", err.Error()) @@ -195,34 +166,6 @@ func TestContains_TopLevelError(t *testing.T) { assert.False(t, xerror.Contains(err, "fmt p1")) } -func TestContainsPattern_Method(t *testing.T) { - err := xerror.Wrap(xerror.New("fmt %v x", "p1"), "fmt2 %v y", "p2") - assert.Equal(t, "fmt2 p2 y: fmt p1 x", err.Error()) - assert.True(t, err.ContainsPattern(regexp.MustCompile("%v y$"))) - assert.False(t, err.ContainsPattern(regexp.MustCompile("p2 y$"))) - assert.True(t, err.ContainsPattern(regexp.MustCompile("%v x$"))) - assert.False(t, err.ContainsPattern(regexp.MustCompile("p1 x$"))) -} - -func TestContainsPattern_TopLevelNilErr(t *testing.T) { - assert.False(t, xerror.ContainsPattern(nil, regexp.MustCompile("r"))) -} - -func TestContainsPattern_TopLevelNativeErr(t *testing.T) { - err := errors.New("msg") - assert.True(t, xerror.ContainsPattern(err, regexp.MustCompile("^m"))) - assert.False(t, xerror.ContainsPattern(err, regexp.MustCompile("^e"))) -} - -func TestContainsPattern_TopLevelError(t *testing.T) { - err := xerror.Wrap(xerror.New("fmt %v x", "p1"), "fmt2 %v y", "p2") - assert.Equal(t, "fmt2 p2 y: fmt p1 x", err.Error()) - assert.True(t, xerror.ContainsPattern(err, regexp.MustCompile("%v y$"))) - assert.False(t, xerror.ContainsPattern(err, regexp.MustCompile("p2 y$"))) - assert.True(t, xerror.ContainsPattern(err, regexp.MustCompile("%v x$"))) - assert.False(t, xerror.ContainsPattern(err, regexp.MustCompile("p1 x$"))) -} - func TestClone_FormatOnly(t *testing.T) { err := xerror.New("fmt") cp := err.Clone() @@ -246,7 +189,6 @@ func TestImplementsError(t *testing.T) { } func TestMarshalJSON(t *testing.T) { - xerr := xerror.New("fmt %v", "p1", "d2", "d1") - _, err := json.Marshal(xerr) + _, err := json.Marshal(xerror.New("fmt %v", "p1", "d2", "d1")) assert.Nil(t, err) } From 5268eeac289c3c2a2735998f7815daa090725188 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 16:34:45 -0700 Subject: [PATCH 06/17] Update README.md --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a4ef170..45faaa0 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,6 @@ We will now learn how to create errors, propagate them, check for error types, a ##### Creating a new error -- https://godoc.org/gopkg.in/ibrt/go-xerror.v2/xerror#New - ```go // Defining each error type as a constant string is a good practice. const ErrorInvalidValueForField = "invalid value for field %v" @@ -38,10 +36,20 @@ const ErrorInvalidValueForField = "invalid value for field %v" // Since it's public and possibly part of a library, we return error. func ValidateRequest(r *Request) error { if r.UserID == "" { - // "userId" replaces the %v placeholder in the error string + // "userId" replaces the %v placeholder in the error type string // extra arguments such as `request` are instead only attached to the debug objects slice return xerror.New(ErrorInvalidValueForField, "userId", request) } return nil } ``` + +Calling the Error interface methods on the newly created error would return the following: + +``` +err.Error() -> "invalid value for field userId" +err.Debug() -> []interface{}{"userId", request} +err.Stack() -> a slice of strings representing the stack at call time +``` + +Please note that all arguments 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. From fd071104c1fb145304c9b59049cc7374307d7f48 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 17:04:10 -0700 Subject: [PATCH 07/17] Update README.md --- README.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 45faaa0..2d5a9f7 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ We will now learn how to create errors, propagate them, check for error types, a ##### Creating a new error +To create a new error, use the `xerror.New` function: + ```go // Defining each error type as a constant string is a good practice. const ErrorInvalidValueForField = "invalid value for field %v" @@ -49,7 +51,57 @@ Calling the Error interface methods on the newly created error would return the ``` err.Error() -> "invalid value for field userId" err.Debug() -> []interface{}{"userId", request} -err.Stack() -> a slice of strings representing the stack at call time +err.Stack() -> a slice of strings representing the stack at New call ``` Please note that all arguments 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. + +##### 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: + +``` +err.Error() -> "bad request for 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: + +``` +err.Error() -> "bad request for 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 +} +``` From cdd42abc6ecc9f026bbb8cd64e81cb393cfa4c47 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 17:17:13 -0700 Subject: [PATCH 08/17] Update README.md --- README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d5a9f7..16973f3 100644 --- a/README.md +++ b/README.md @@ -103,5 +103,26 @@ Calling the Error interface methods on the second error would return the followi err.Error() -> "bad request for 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`, which 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. The functions described above instead perform the comparison on the format string, allowing to generate clearer error messages while retaining the ability to check error types. + +Let's consider the second error from the _Propagating errors_ section. Here is the result of some sample calls: + +``` +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 match on errors at any level. 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. + +``` +xerror.Is(secondError, ErrorBadRequest) -> true +xerror.Is(secondError, ErrorMalformedRequestBody) -> false +xerror.Is(errors.New(ErrorBadRequest), ErrorBadRequest) -> true +xerror.Contains(errors.New(ErrorBadRequest), ErrorBadRequest -> true ``` From ac730dd98b7b5e0533a0722f0f33993b543096ef Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 17:18:23 -0700 Subject: [PATCH 09/17] Update README.md --- README.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 16973f3..4daeabb 100644 --- a/README.md +++ b/README.md @@ -48,10 +48,10 @@ func ValidateRequest(r *Request) error { Calling the Error interface methods on the newly created error would return the following: -``` -err.Error() -> "invalid value for field userId" -err.Debug() -> []interface{}{"userId", request} -err.Stack() -> a slice of strings representing the stack at New call +```go +err.Error() // -> "invalid value for field userId" +err.Debug() // -> []interface{}{"userId", request} +err.Stack() // -> a slice of strings representing the stack at New call ``` Please note that all arguments 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. @@ -91,18 +91,18 @@ func HandleRequest(r *http.Request) (*Response, error) { Calling the Error interface methods on the first error would return the following: -``` -err.Error() -> "bad request for URL : unexpected end of file" -err.Debug() -> []interface{}{r.URL, r} -err.Stack() -> a slice of strings representing the stack at Wrap call +```go +err.Error() // -> "bad request for 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: -``` -err.Error() -> "bad request for 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 +```go +err.Error() // -> "bad request for 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 @@ -111,18 +111,18 @@ This library provides functions for determining error types: `Is` and `Contains` Let's consider the second error from the _Propagating errors_ section. Here is the result of some sample calls: -``` -err.Is(ErrorBadRequest) -> true -err.Is(ErrorMalformedRequestBody) -> false -err.Contains(ErrorBadRequest) -> true -err.Contains(ErrorMalformedRequestBody) -> true +```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 match on errors at any level. 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. -``` -xerror.Is(secondError, ErrorBadRequest) -> true -xerror.Is(secondError, ErrorMalformedRequestBody) -> false -xerror.Is(errors.New(ErrorBadRequest), ErrorBadRequest) -> true -xerror.Contains(errors.New(ErrorBadRequest), ErrorBadRequest -> true +```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 ``` From ec62b5712ed51e62b2c2b3325fc9db5ac8420dd7 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 17:53:38 -0700 Subject: [PATCH 10/17] Update README.md --- README.md | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 4daeabb..523d558 100644 --- a/README.md +++ b/README.md @@ -19,43 +19,36 @@ This package is particularly useful in applications such as API servers, where e - 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 ### 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 attempts to describe and clarify the best practices for error handling in Go. +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 -// Defining each error type as a constant string is a good practice. const ErrorInvalidValueForField = "invalid value for field %v" -// The return type of this function could alternatively be xerror.Error. -// Since it's public and possibly part of a library, we return error. func ValidateRequest(r *Request) error { if r.UserID == "" { - // "userId" replaces the %v placeholder in the error type string - // extra arguments such as `request` are instead only attached to the debug objects slice return xerror.New(ErrorInvalidValueForField, "userId", request) } return nil } ``` -Calling the Error interface methods on the newly created error would return the following: +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 at New call +err.Stack() // -> a slice of strings representing the stack when New is called ``` -Please note that all arguments 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. - ##### 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: @@ -92,7 +85,7 @@ func HandleRequest(r *http.Request) (*Response, error) { Calling the Error interface methods on the first error would return the following: ```go -err.Error() // -> "bad request for URL : unexpected end of file" +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 ``` @@ -100,14 +93,14 @@ 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 : malformed request body: invalid character 'b'" -err.Debug() // -> []interface{}{r.URL, r, buf +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`, which 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. The functions described above instead perform the comparison on the format string, allowing to generate clearer error messages while retaining the ability to check error types. +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: @@ -118,7 +111,7 @@ 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 match on errors at any level. 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. +In other words, `Is` only compares the format string with the outermost error in the wrap chain, while `Contains` performs the comparisone 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 From dd7af862c74e3a11df631edefb26aa99fa5bba13 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 17:54:19 -0700 Subject: [PATCH 11/17] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 523d558..c9ebb52 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ To create a new error, use the `xerror.New` function: const ErrorInvalidValueForField = "invalid value for field %v" func ValidateRequest(r *Request) error { - if r.UserID == "" { + if r.UserID <= 0 { return xerror.New(ErrorInvalidValueForField, "userId", request) } return nil From 93d944f23c046eee89376dd6bef7640c1e2eab5d Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 18:04:09 -0700 Subject: [PATCH 12/17] Implement GoStringer on Error. --- xerror/error.go | 10 ++++++++++ xerror/error_test.go | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/xerror/error.go b/xerror/error.go index 4957d99..e51cdd6 100644 --- a/xerror/error.go +++ b/xerror/error.go @@ -15,6 +15,7 @@ import ( type Error interface { error json.Marshaler + fmt.GoStringer Is(string) bool Contains(string) bool @@ -73,6 +74,15 @@ func (e *xerr) MarshalJSON() ([]byte, error) { }) } +// 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) +} + // 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 diff --git a/xerror/error_test.go b/xerror/error_test.go index c27ba28..3a959cf 100644 --- a/xerror/error_test.go +++ b/xerror/error_test.go @@ -3,6 +3,7 @@ package xerror_test import ( "encoding/json" "errors" + "fmt" "github.com/ibrt/go-xerror/xerror" "github.com/stretchr/testify/assert" "testing" @@ -192,3 +193,11 @@ func TestMarshalJSON(t *testing.T) { _, err := json.Marshal(xerror.New("fmt %v", "p1", "d2", "d1")) assert.Nil(t, err) } + +func TestFormat(t *testing.T) { + err := xerror.New("fmt %v", "p1", "d1") + assert.Equal(t, "fmt p1", fmt.Sprintf("%v", err)) + buf, err2 := err.MarshalJSON() + assert.Nil(t, err2) + assert.Equal(t, string(buf), fmt.Sprintf("%#v", err)) +} From d1caab0036cc275a45100a82e0acb44bf358d603 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 18:05:51 -0700 Subject: [PATCH 13/17] Minor refactoring in tests. --- xerror/error_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xerror/error_test.go b/xerror/error_test.go index 3a959cf..2d2f194 100644 --- a/xerror/error_test.go +++ b/xerror/error_test.go @@ -189,12 +189,12 @@ func TestImplementsError(t *testing.T) { assert.True(t, ok) } -func TestMarshalJSON(t *testing.T) { +func TestImplementsJSONMarshaler(t *testing.T) { _, err := json.Marshal(xerror.New("fmt %v", "p1", "d2", "d1")) assert.Nil(t, err) } -func TestFormat(t *testing.T) { +func TestImplementsFMTStringer(t *testing.T) { err := xerror.New("fmt %v", "p1", "d1") assert.Equal(t, "fmt p1", fmt.Sprintf("%v", err)) buf, err2 := err.MarshalJSON() From bed1ba4b880fc2e4cdaeda507d82f5234203c87b Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 18:07:50 -0700 Subject: [PATCH 14/17] Check that GoStringer implementation works also when cast to error. --- xerror/error_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xerror/error_test.go b/xerror/error_test.go index 2d2f194..18be233 100644 --- a/xerror/error_test.go +++ b/xerror/error_test.go @@ -196,8 +196,8 @@ func TestImplementsJSONMarshaler(t *testing.T) { func TestImplementsFMTStringer(t *testing.T) { err := xerror.New("fmt %v", "p1", "d1") - assert.Equal(t, "fmt p1", fmt.Sprintf("%v", err)) + assert.Equal(t, "fmt p1", fmt.Sprintf("%v", error(err))) buf, err2 := err.MarshalJSON() assert.Nil(t, err2) - assert.Equal(t, string(buf), fmt.Sprintf("%#v", err)) + assert.Equal(t, string(buf), fmt.Sprintf("%#v", error(err))) } From 0f0c3e081677aa0f8654db671de0644cdfd913e3 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 18:35:06 -0700 Subject: [PATCH 15/17] Update README.md --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index c9ebb52..298282f 100644 --- a/README.md +++ b/README.md @@ -119,3 +119,33 @@ xerror.Is(secondError, ErrorMalformedRequestBody) // -> false xerror.Is(errors.New(ErrorBadRequest), ErrorBadRequest) // -> true xerror.Contains(errors.New(ErrorBadRequest), ErrorBadRequest // -> true ``` + +##### Reporting and displaying errors + +The `xerror.Error` interface extends `error`, `json.Marshaler`, and `fmt.GoStringer`. It is possible to obtain string representations of errors for various use cases: + +- calling `err.Error()` or formatting as `%s` or `%v`returns a short string +- serializing to JSON or formatting as `%#v` returns a long string + +This is an example of short string: + +``` +bad request: malformed request body: invalid character 'b' +``` + +This is an example of long string (actually on a single line): + +``` +{ + "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)", + ... + ] +} +``` From 9a0cf53f980ff63118341c2c265af2b693705188 Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 18:35:35 -0700 Subject: [PATCH 16/17] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 298282f..e7ff08e 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ 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 comparisone 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. +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 From f30a2d32e76a90fd82fa4066f886e5b7cba3630f Mon Sep 17 00:00:00 2001 From: Ivan Bertona Date: Wed, 13 Apr 2016 18:45:31 -0700 Subject: [PATCH 17/17] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e7ff08e..bf47327 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ In other words, `Is` only compares the format string with the outermost error in xerror.Is(secondError, ErrorBadRequest) // -> true xerror.Is(secondError, ErrorMalformedRequestBody) // -> false xerror.Is(errors.New(ErrorBadRequest), ErrorBadRequest) // -> true -xerror.Contains(errors.New(ErrorBadRequest), ErrorBadRequest // -> true +xerror.Contains(errors.New(ErrorBadRequest), ErrorBadRequest) // -> true ``` ##### Reporting and displaying errors