Skip to content
Snippets Groups Projects
Select Git revision
  • a4c71c057874b3781438f301b3f8857a5878f059
  • main default protected
  • v1.303.0
  • v1.302.0
  • v1.301.0
  • v1.300.0
  • v1.299.0
  • v1.298.0
  • v1.297.0
  • v1.296.0
  • v1.295.0
  • v1.294.0
  • v1.293.0
  • v1.292.0
  • v1.291.0
  • v1.290.0
  • v1.289.0
  • v1.288.0
  • v1.287.0
  • v1.286.0
  • v1.285.0
  • v1.284.0
22 results

README.md

Blame
  • user avatar
    Jan Semmelink authored
    22d978fe
    History

    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.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) 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.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.