juju/errgo

How to get the original error?

Soulou opened this issue · 5 comments

Let's imagine the following scenario.

type CustomError struct {}
func (err *CustomError) Error() string { … }

func a() error {
   return &CustomError{}
}

func b() error {
   err := a()
   if err != nil {
     return errgo.Mask(err)
  }
}

func c() error {
   err := b()
   if err != nil {
     return errgo.Mask(err)
  }
}

func main() {
  err := c()
  if err != nil {
     // Here I want to check if the source of err is a CustomError, how can I do it ?
  }
}

The example speaks for itself I think, when several levels of masking are applied, when the error is returning to the "upper" level of the application (http request handler, or anything else), what is the proper way to track its origin?

Thanks

This example speaks directly to one of the main reasons for errgo's existence.

If callers of c are checking for a possible CustomError error, then
the CustomError should be part of the contract of c.
When using errgo, the way to state an error contract is to add an argument
to Mask to allow that specific error (or some class of errors) to
pass through and not be masked.

Your example would look something like this:

package main
import (
    "fmt"
    "gopkg.in/errgo.v1"
)

type CustomError struct{}

func (err *CustomError) Error() string {
    return "custom error"
}

func IsCustomError(err error) bool {
    _, ok := err.(*CustomError)
    return ok
}

func a() error {
    return &CustomError{}
}

func b() error {
    err := a()
    if err != nil {
        return errgo.Mask(err, IsCustomError)
    }
    return nil
}

func c() error {
    err := b()
    if err != nil {
        return errgo.Mask(err, IsCustomError)
    }
    return nil
}

func main() {
    err := c()
    if IsCustomError(errgo.Cause(err)) {
        fmt.Printf("got custom error\n")
    }
}

Doing things this way means that you have to explicitly
think about possible error return types for each function
that you write. This is a Good Thing, IMHO.

  • If c happens to call other functions
    and one of those happens to return a CustomError,
    the code in main will not behave erroneously, expecting
    the error to have been encountered when running a.
  • If we change b to call some function other than a,
    it should be much less likely to break the code in main,
    because it's evident that the CustomError is part of b's
    contract, and should therefore be part of its tests.
  • For the majority of functions where CustomError is not
    mentioned in the Mask call, we can change the code freely,
    knowing that callers cannot be relying on the returned error
    type. Error types are often poorly documented in Go, so
    developers will often run some code, see what error type
    is returned, and write logic based on that. This is a recipe for
    fragile code, because that can make a dependency between
    several unconnected logical layers.

Note that if you do want to return any error cause unchanged
(for example because you're calling a user-supplied callback),
you can use errgo.Any.

See https://rogpeppe.wordpress.com/2014/04/23/some-suggested-rules-for-generating-good-errors-in-go/ for some further exposition.

For the record, the canonical path for errgo is now gopkg.in/errgo.v1

Thank you for your comprehensive answer, it is more clear now.

But from that, I've another interogation. If a can return different types of errors, is the errgo-ish way to write it is like this:

package main
import (
    "fmt"
    "gopkg.in/errgo.v1"
)

type CustomErrorA struct{}
type CustomErrorB struct{}

func (err *CustomErrorA) Error() string {
    return "custom error a"
}
func (err *CustomErrorB) Error() string {
    return "custom error b"
}

func IsCustomErrorA(err error) bool {
    _, ok := err.(*CustomErrorA)
    return ok
}
func IsCustomErrorB(err error) bool {
    _, ok := err.(*CustomErrorB)
    return ok
}

func a() error {
    if something {
        return &CustomErrorA{}
    } else {
        return &CustomErrorB{}
    }
}

func b() error {
    err := a()
    if err != nil {
        if IsCustomErrorA(errgo.Cause(err)) {
            return errgo.Mask(err, IsCustomErrorA)
        } else if IsCustomErrorB(errgo.Cause(err)) {
            return errgo.Mask(err, IsCustomErrorB)
        } else {
            return errgo.Mask(err)
        }
    }
    return nil
}

func c() error {
    err := b()
    if err != nil {
        if IsCustomErrorA(errgo.Cause(err)) {
            return errgo.Mask(err, IsCustomErrorA)
        } else if IsCustomErrorB(errgo.Cause(err)) {
            return errgo.Mask(err, IsCustomErrorB)
        } else {
            return errgo.Mask(err)
        }
    }
    return nil
}

func main() {
    err := c()
    if IsCustomErrorA(errgo.Cause(err)) {
        fmt.Printf("got custom error A\n")
    }
    if IsCustomErrorB(errgo.Cause(err)) {
        fmt.Printf("got custom error B\n")
    }
}

It's a lot of code to repeat when there are several types of errors. I think I may be completely wrong, could you give me a hint for that ?

There are a few ways to do that.
If a function can return a one-off set of errors, you can add extra arguments to the Mask call, like this:

package main

import (
    "fmt"
    "gopkg.in/errgo.v1"
)

type CustomErrorA struct{}
type CustomErrorB struct{}

func (err *CustomErrorA) Error() string {
    return "custom error a"
}
func (err *CustomErrorB) Error() string {
    return "custom error b"
}

func IsCustomErrorA(err error) bool {
    _, ok := err.(*CustomErrorA)
    return ok
}
func IsCustomErrorB(err error) bool {
    _, ok := err.(*CustomErrorB)
    return ok
}

func a() error {
    if something {
        return &CustomErrorA{}
    } else {
        return &CustomErrorB{}
    }
}

func b() error {
    err := a()
    if err != nil {
        return errgo.Mask(err, IsCustomErrorA, IsCustomErrorB)
    }
    return nil
}

func c() error {
    err := b()
    if err != nil {
        return errgo.Mask(err, IsCustomErrorA, IsCustomErrorB)
    }
    return nil
}

func main() {
    err := c()
    if IsCustomErrorA(errgo.Cause(err)) {
        fmt.Printf("got custom error A\n")
    }
    if IsCustomErrorB(errgo.Cause(err)) {
        fmt.Printf("got custom error B\n")
    }
}

Alternatively, if you have many places that can return the same set
of errors, you could define a function that allows both kinds of
custom error, and use that:

func IsSomeCustomError(err error) bool {
    return IsCustomErrorA(err) || IsCustomErrorB(err)
}

As another alternative, if all error returns in a given package may
return some common subset of errors, you could define your
own custom mask function:

var mask = errgo.MaskFunc(IsCustomErrorA, IsCustomErrorB)

Then you could directly substitute mask for errgo.Mask throughout that
package (attaching additional possible error causes as needed).

Ok, perfect, thank you very much for your answers, it has been very helpful, thank you for errgo !

Maybe those examples could be copied in the README or Wiki of the library (or as an example to see it on godoc), I think it can help others :-)