Search code examples
gopointerstypeserror-handlingerror-checking

Go errors: Is() and As() claim to be recursive, is there any type that implements the error interface and supports this recursion - bug free?


Everywhere I look, the "way" to "wrap" errors in Go is to use fmt.Errof with the %w verb

https://go.dev/blog/go1.13-errors

However, fmt.Errorf does not recursively wrap errors. There is no way to use it to wrap three previously defined errors (Err1, Err2, and Err3) and then check the result by using Is() and get true for each those three errors.

FINAL EDIT:

Thanks to @mkopriva's answer and comments below it, I now have a straightforward way to implement this (although, I am still curious if there is some standard type which does this). In the absence of an example, my attempts at creating one failed. The piece I was missing was adding an Is and As method to my type. Because the custom type needs to contain an error and a pointer to the next error, the custom Is and As methods allows us to compare the error contained in the custom type, rather than the custom type itself.

Here is a working example: https://go.dev/play/p/6BYGgIb728k

Highlights from the above link

type errorChain struct {
    err  error
    next *errorChain
}

//These two functions were the missing ingredient
//Defined this way allows for full functionality even if
//The wrapped errors are also chains or other custom types

func (c errorChain) Is(err error) bool { return errors.Is(c.err, err) }

func (c errorChain) As(target any) bool { return errors.As(c.err, target) }

//Omitting Error and Unwrap methods for brevity

func Wrap(errs ...error) error {
    out := errorChain{err: errs[0]}

    n := &out
    for _, err := range errs[1:] {
        n.next = &errorChain{err: err}
        n = n.next
    }
    return out
}

var Err0 = errors.New("error 0")
var Err1 = errors.New("error 1")
var Err2 = errors.New("error 2")
var Err3 = errors.New("error 3")

func main() {
    //Check basic Is functionality
    errs := Wrap(Err1, Err2, Err3)
    fmt.Println(errs)                            //error 1: error 2: error 3
    fmt.Println(errors.Is(errs, Err0))           //false
    fmt.Println(errors.Is(errs, Err2))           //true
}

While the Go source specifically mentions the ability to define an Is method, the example does not implement it in a way that can solve my issue and the discussion do not make it immediately clear that it would be needed to utilize the recursive nature of errors.Is.

AND NOW BACK TO THE ORIGINAL POST:

Is there something built into Go where this does work?

I played around with making one of my own (several attempts), but ran into undesirable issues. These issues stem from the fact that errors in Go appear to be compared by address. i.e. if Err1 and Err2 point to the same thing, they are the same.

This causes me issues. I can naively get errors.Is and errors.As to work recursively with a custom error type. It is straightforward.

  1. Make a type that implements the error interface (has an Error() string method)
  2. The type must have a member that represents the wrapped error which is a pointer to its own type.
  3. Implement an Unwrap() error method that returns the wrapped error.
  4. Implement some method which wraps one error with another

It seems good. But there is trouble.

Since errors are pointers, if I make something like myWrappedError = Wrap(Err1, Err2) (in this case assume Err1 is being wrapped by Err2). Not only will errors.Is(myWrappedError, Err1) and errors.Is(myWrappedError, Err2) return true, but so will errors.Is(Err2, Err1)

Should the need arise to make myOtherWrappedError = Wrap(Err3, Err2) and later call errors.Is(myWrappedError, Err1) it will now return false! Making myOtherWrappedError changes myWrappedError.

I tried several approaches, but always ran into related issues.

Is this possible? Is there a Go library which does this?

NOTE: I am more interested in the presumably already existing right way to do this rather than the specific thing that is wrong with my basic attempt

Edit 3: As suggested by one of the answers, the issue in my first code is obviously that I modify global errors. I am aware, but failed to adequately communicate. Below, I will include other broken code which uses no pointers and modifies no globals.

Edit 4: slight modification to make it work more, but it is still broken

See https://go.dev/play/p/bSytCysbujX

type errorGroup struct {
    err        error
    wrappedErr error
}

//...implemention Unwrap and Error excluded for brevity

func Wrap(inside error, outside error) error {
    return &errorGroup{outside, inside}
}

var Err1 = errorGroup{errors.New("error 1"), nil}
var Err2 = errorGroup{errors.New("error 2"), nil}
var Err3 = errorGroup{errors.New("error 3"), nil}

func main() {
    errs := Wrap(Err1, Err2)
    errs = Wrap(errs, Err3)
    fmt.Println(errs)//error 3: error 2: error 1
    fmt.Println(errors.Is(errs, Err1)) //true
    fmt.Println(errors.Is(errs, Err2)) //false <--- a bigger problem
    fmt.Println(errors.Is(errs, Err3)) //false <--- a bigger problem
}

Edit 2: playground version shortened

See https://go.dev/play/p/swFPajbMcXA for an example of this.

EDIT 1: A trimmed version of my code focusing on the important parts:

type errorGroup struct {
    err        error
    wrappedErr *errorGroup
}

//...implemention Unwrap and Error excluded for brevity

func Wrap(errs ...*errorGroup) (r *errorGroup) {
    r = &errorGroup{}
    for _, err := range errs {
        err.wrappedErr = r
        r = err

    }
    return
}

var Err0 = &errorGroup{errors.New("error 0"), nil}
var Err1 = &errorGroup{errors.New("error 1"), nil}
var Err2 = &errorGroup{errors.New("error 2"), nil}
var Err3 = &errorGroup{errors.New("error 3"), nil}

func main() {
    errs := Wrap(Err1, Err2, Err3)//error 3: error 2: error 1
    fmt.Println(errors.Is(errs, Err1)) //true

    //Creating another wrapped error using the Err1, Err2, or Err3 breaks the previous wrap, errs.
    _ = Wrap(Err0, Err2, Err3)
    fmt.Println(errors.Is(errs, Err1)) //false <--- the problem
}

Solution

  • You can use something like this:

    type errorChain struct {
        err  error
        next *errorChain
    }
    
    func Wrap(errs ...error) error {
        out := errorChain{err: errs[0]}
    
        n := &out
        for _, err := range errs[1:] {
            n.next = &errorChain{err: err}
            n = n.next
        }
        return out
    }
    
    func (c errorChain) Is(err error) bool {
        return c.err == err
    }
    
    func (c errorChain) Unwrap() error {
        if c.next != nil {
            return c.next
        }
        return nil
    }
    

    https://go.dev/play/p/6oUGefSxhvF