Home > Software engineering >  Go errors: Is() and As() claim to be recursive, is there any type that implements the error interfac
Go errors: Is() and As() claim to be recursive, is there any type that implements the error interfac

Time:03-29

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.

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 Is() and 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
}

CodePudding user response:

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

CodePudding user response:

Your code modifies package-global error values, so it is inherently broken. This defect has nothing to do with Go's error handling mechanics.

Per the documentation you linked, there are two error-handling helpers: Is, and As. Is lets you recursively unwrap an error, looking for a specific error value, which is necessarily a package global for this to be useful. As, on the other hand, lets you recursively unwrap an error looking for any wrapped error value of a given type.

How does wrapping work? You wrap error A in a new error value B. A Wrap() helper would necessarily return a new value, as fmt.Errorf does in the examples in the linked documentation. A Wrap helper should never modify the value of the error being wrapped. That value should be considered immutable. In fact, in any normal implementation, the value would be of type error, so that you can wrap any error, rather than just wrapping concentric values of your custom error type in each other; and, in that case, you have no access to the fields of the wrapped error to modify them anyway. Essentially, Wrap should be roughly:

func Wrap(err error) error {
    return &errGroup{err}
}

And that's it. That's not very useful, because your implementation of errGroup doesn't really do anything - it provides no details about the error that occurred, it's just a container for other errors. For it to have value, it should have a string error message, or methods like some other error types' IsNotFound, or something that makes it more useful than just using error and fmt.Errorf.

Based on the usage in your example code, it also looks like you're presuming the use case is to say "I want to wrap A in B in C", which I've never seen in the wild and I cannot think of any scenario where that would be needed. The purpose of wrapping is to say "I've recieved error A, I'm going to wrap it in error B to add context, and return it". The caller might wrap that error in error C, and so on, which is what makes recursive wrapping valuable.

For example:

type MyError struct {
    message string
    wrapped error
}

func (e MyError) Error() string {
    return e.message
}

func NewError(message string) error {
    return &MyError{message, nil}
}

func WrapError(message string, child error) error {
    return &MyError{message, child}
}

https://go.dev/play/p/LO5yGk3z51R

CodePudding user response:

There arr several approaches but there is one thing that you should keep in mind: if you have multiple errors, you may need to handle it as a slice of errors

For instance, imagine you need to check if all errors are the same, or there is at least one error of certain type you can use the snippet below.

You can extend this concept or use some existing library to handle multierrors

type Errors []error

func (errs Errors) String() string {
  …
}

func (errs Errors) Any(target error) bool{
    for _, err := range errs {
        if errors.Is(err,target) {
            return true
        }
    }
    return false
}

func (errs Errors) All(target error) bool{
    if len(errs) == 0 { return false }
    
    for _, err := range errs {
        if !errors.Is(err,target) {
            return false
        }
    }
    return true
}
  • Related