Skip to content

ameteiko/errors

Repository files navigation

Go errors

Overview

This module introduces the fuctionality of handling Go errors as first-class citizens by:

  • providing a way to attach inspectable context to an existing error;
  • making errors' contexts inspectable;
  • making errors unit-testing friendly.

This module was inspired by the github.com/pkg/errors one which solves some error handling issues in Go, but still is hardly usable for the unit-testing.

All errors returned from the utility functions Wrap, WithMessage and WrapWithMessage return an object conforming to the error interface with a stacktrace created at the moment of function invocation.

err := errors.Wrap(errors.New("inner error"), errors.New("outer error"))
fmt.Printf("%s", err)
// Output: 
// outer error : inner error
fmt.Printf("%+v", err)
// Output: 
// outter error : inner error
//    github.com/ameteiko/errors/errors.go:52 errors.Wrap()
//    github.com/ameteiko/errors/examples/main.go:10 main.main()

The error message consists of all error messages joined with " : " sequence.

More in detail

Internally all errors are merged into an internal error queue object with a stacktrace at the moment of creation. It's recommended to have a single error-flow path for the application, meaning that parameters to the Wrap function must not be composite errors coming from different executions paths, because merging them won't make much sense from the operational perspective.

Installation**

go get github.com/ameteiko/errors

New(format string, args ...interface{}) error

This function is a wrapper for the fmt.Errorf function to allow use a single package for error handling.

err := errors.New("validation failed for %q", username)

Wrap(err ...error) error

Utility function Wrap merges several errors passed as a variadic parameters into one. All nil errors will be filtered out of the resulting error. The recommended way of using the function is to attach an application error to the error returned from the 3rd-party module.

// Some package-level error
var errParsing = errors.New("parsing error")

func parseResponse(r []byte) error {
    var resp Response
    if err := json.Unmarshal(r, &resp); err != nil {
        // Wrap the error returned from the json package with application identifiable error.
        return errors.Wrap(err, errParsing) 
    }
}

If several errors contain stacktraces, the new one will be created at the moment of the Wrap instantiation. If only one contains a stacktrace, then it will be reused.

WithMessage(err error, format string, args ...interface{}) error

WithMessage attaches a formatted message for the error. Best used to add some error context to the error, like parameters that caused an error.

func parseResponse(r []byte) error {
    var resp Response
    if err := json.Unmarshal(r, &resp); err != nil {
        return errors.WithMessage(err, "unmarshalling error for %v", r)
    }
}

WrapWithMessage(erra, errb error, format string, args ...interface{}) error

WrapWithMessage wraps several errors and attaches a message to them. This function is a combination of Wrap and WithMessage.

var errParsing = errors.New("parsing error")

func parseResponse(r []byte) error {
    var resp Response
    if err := json.Unmarshal(r, &resp); err != nil {
        // Wrap the error returned from the json package with application identifiable error and a message.
        return errors.WrapWithMessage(err, errParsing, "parsing error for %v", r)
    }
}

Fetch(source error, target error) error

Inspects the source error and returns matched target error object from it.

parsingErr := parseResponse()
if err := errors.Fetch(parsingErr, errParsing); err != nil {
    log.Println("parsing error", err)
}

FetchByType(source error, target interface{}) error

Inspects the error and returns the entry matched by the type. Returns nil if If target is nil or not a pointer.

// LoggableErr log error messages when they are handled.
type LoggableErr struct {
    msg string
}

func (e LoggableErr) Error() string { return e.msg }
func (e LoggableErr) Log() string { log.Println(e.msg) }

errParsing := LoggableErr{msg: "parsing error"}
  
func main() {
    parsingErr := parseResponse("}")
    if err := errors.FetchByType(parsingErr, (*LoggableErr)(nil)); err != nil {
        loggableErr := err.(LoggableErr)
        loggableErr.Log()
    }
}

FetchAllByType(source error, target interface{}) error

Inspects the error and returns all the entries matched by the type. Returns nil if If target is nil or not a pointer.

type ValidationError struct {
    msg string
}

func (e ValidationError) Error() string { return e.msg }

var (
    errNameIsEmpty    = ValidationError{ msg: "name is empty" }
    errNameIsTooLong  = ValidationError{ msg: "name is too long" }
    errNameIsTooShort = ValidationError{ msg: "name is too short" }
    errPwdIsEmpty     = ValidationError{ msg: "password is empty" }
)

func validateUser(name, pwd string) (err error) {
    if name == "" {
        err = errors.Wrap(err, errNameIsEmpty)
    }
    if pwd == "" {
        err = errors.Wrap(err, errPwdIsEmpty)
    }
    // ... the rest of validations
  
    return err 
}

func main() {
    err := validateUser("", "")
    errs := errors.FetchAllByType(err, (*ValidationError)(nil))
    fmt.Println(errs)
    // Prints two errors: errPwdIsEmpty, errNameIsEmpty
}