errors
This is our own implementation of the Golang error
interface.
Why
Packages tend to use one of the following to return errors:
- Standard go
"errors"
package witherrors.New("...")
- Standard go
"fmt"
package withfmt.Errorf("...")
-
github.com/pkg/errors
package witherrors.New("...")
Of those three, the pkg/errors is by far the better option as it includes an error stack.
One could use pkg/errors, but:
- it does not offer much flexibility in how it displays errors
- its display is cluttered, with the wrong info (e.g. runtime stack and not the error handling stack)
- it does not display the up passing of errors in the stack unless you add the stack each time
- if you do add the stack each time, it dumps many copies of a stack and becomes cluttered
We write our own version of errors, to:
- always add the stack
- add the correct stack of where we raised/handled the error
- make error handling simpler (Wrapf(err) vs WithMessage(WithStack(err)))
- give us all the flexibility we need
- e.g. give us option to output JSON structured stack
For lots of detail on this, see comments in error_formats_test.go and run that test with go test -v error_formats_test.go
...
Usage
Get the package into your project:
go get gitlab.com/uafrica/go-utils
func New()
Fail with your own simple error message:
if !authenticated {
return errors.New("not authenticated")
}
func Errorf()
Fail with your own error using Printf style, e.g. when you detect an invalid value:
if limit < 0 {
return errors.Errorf("invalid limit %d<0", limit)
}
func Wrapf()
Fail when a called function returned an error:
if err := db.Select(query); err != nil {
return errors.Wrapf(err, "query failed on user=%s", username)
}
func HTTP()
Create an HTTP error, which is the same as others but includes and HTTP Status code:
if err := db.Select(query); err != nil {
return errors.HTTP(http.StatusNotFound, err, "cannot read users")
}
To retrieve the code, use err.Code() which returns integer value.
If you wrapped an HTTP error in another HTTP error, there may be different codes in the stack. The Code() method will dig down all the causes and return the lowest non-zero code value it finds. So StatusNotFound (404) will have precedence over StatusInsufficientStorage (507) It will return 0 when there are no codes in the error stack.
An error with HTTP code will also print the code in the stack, e.g.:
http-error_test.go:27: failed:
http-error_test.go(24): TestHTTPError() terrible mistake HTTP(404:Not Found), because
http-error_test.go(21): TestHTTPError() jissis this is bad! HTTP(507:Insufficient Storage), because
http-error_test.go(18): TestHTTPError() failed to get user HTTP(400:Bad Request), because
http-error_test.go(14): TestHTTPError() failed to connect to db
Refactoring exiting code
Replace all other errors package imports with this package:
import (
"gitlab.com/uafrica/go-utils/errors"
)
Refactor from standad go "errors" package:
- No change:
errors.New()
is still supported in this package
Refactor from "fmt" package:
- Replace
errors.Errorf("my message: %v", err)
witherrors.Wrapf(err, "my message")
so that the layers are preserved and not merged into a single text string.
Refactor from "github.com/pkg/errors":
- Replace
errors.WithStack(err)
witherrors.New(err)
. - Replace
return err
withreturn errors.Wrapf(err, "some message")
saying what failed as result of the err - Replace
errors.WithMessagef(err, "...", ...)
withreturn errors.Wrapf(err, "...", ...)
- Replace
errors.WithMessage(err, "...")
withreturn errors.Wrap(err, "...")
Formatting
Report an error with:
user,err := getUser(userName)
if err != nil {
log.Errorf("failed to get user %s: %+v", username, err)
...
}
Select the appropriate format:
-
%+c
in most cases to write the full compact error with file names and line numbers all on one line. -
%+v
is the same when you write to console over multipole lines -
%c
is full error on one line, without reference to source code, i.e. concatenate the error message of all levels e.g.A, because B, because C
, which you also get fromerr.Error()
. -
%v
is full error over multipole lines, without references to source code, e.g.
A, because
B, because
C
-
%s
is just the last error message.
JSON error:
Call err.Description()
to get a struct that can marshal to JSON for a complete dump, e.g.:
{
"error": "login failed",
"source": {
"package": "gitlab.com/uafrica/go-utils/errors/errors_test",
"file": "errors_test.go",
"line": 18,
"function": "TestErrorFormatting"
},
"cause": {
"error": "failed to find account",
"source": {
"package": "gitlab.com/uafrica/go-utils/errors/errors_test",
"file": "errors_test.go",
"line": 17,
"function": "TestErrorFormatting"
},
"cause": {
"error": "query failed",
"source": {
"package": "gitlab.com/uafrica/go-utils/errors/errors_test",
"file": "errors_test.go",
"line": 16,
"function": "TestErrorFormatting"
},
"cause": {
"error": "you have problem in your SQL near xxx"
}
}
}
}
func Caller()
This function can be used also for logging to determine the caller from the runtime stack. It takes a skip value to skip a few levels of the stack, making it flexible to be called in various wrapper functions, without reporting the wrappers.
func Stack()
This function is similar to Caller() but reports an array of callers, not only one.
func Is()
You can compare a message with errors.Is() to some error specification. It will look at the error or any cause to match the spec. The spec is the error message.