# 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 with ```errors.New("...")``` * Standard go ```"fmt"``` package with ```fmt.Errorf("...")``` * ```github.com/pkg/errors``` package with ```errors.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.bob.co.za/bob-public-utils/bobgroup-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.bob.co.za/bob-public-utils/bobgroup-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)` with `errors.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)``` with ```errors.New(err)```. - Replace ```return err``` with ```return errors.Wrapf(err, "some message")``` saying what failed as result of the err - Replace ```errors.WithMessagef(err, "...", ...)``` with ```return errors.Wrapf(err, "...", ...)``` - Replace ```errors.WithMessage(err, "...")``` with ```return 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 from ```err.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.bob.co.za/bob-public-utils/bobgroup-go-utils/errors/errors_test", "file": "errors_test.go", "line": 18, "function": "TestErrorFormatting" }, "cause": { "error": "failed to find account", "source": { "package": "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors/errors_test", "file": "errors_test.go", "line": 17, "function": "TestErrorFormatting" }, "cause": { "error": "query failed", "source": { "package": "gitlab.bob.co.za/bob-public-utils/bobgroup-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.