Error wrap/unwrap && type checking with errors.Is()
Asked Answered
A

2

10

I am checking through an error trace in Go v1.13 Go v1.14. Why does it appear that only error implementations without parameters or with value receivers can be found with errors.Is()? This means that an error implementation capable of wrapping must have a value receiver in order to be able to be found with errors.Is().

package main

import (
    "fmt"
    "errors"
)

type someAtomicError struct {}
func (e *someAtomicError) Error() string { return "Hi!" }
func checkAtomicError() {
    e := &someAtomicError{}
    e2 := fmt.Errorf("whoa!: %w", e)
    e2IsE := errors.Is(e2, &someAtomicError{})
    fmt.Println("atomic error trace ---\t\t", e2, "\t\t--- is traceable: ", e2IsE)
}


type someWrapperError struct {
    Msg string
    Err error
}
func (e someWrapperError) Error() string { return fmt.Sprintf("%s: %v", e.Msg, e.Err) }
func (e someWrapperError) Unwrap() error { return e.Err }
func checkWrapperError() {
    e := someWrapperError{"Hi!", nil}
    e2 := fmt.Errorf("whoa!: %w", e)
    e2IsE := errors.Is(e2, someWrapperError{"Hi!", nil})
    fmt.Println("wrapper error trace ---\t\t", e2, "\t--- is traceable: ", e2IsE)
}


type somePointerWrapperError struct {
    Msg string
    Err error
}
func (e *somePointerWrapperError) Error() string { return fmt.Sprintf("%s: %v", e.Msg, e.Err) }
func (e *somePointerWrapperError) Unwrap() error { return e.Err }
func checkPointerWrapperError() {
    e := &somePointerWrapperError{"Hi!", nil}
    e2 := fmt.Errorf("whoa!: %w", e)
    e2IsE := errors.Is(e2, &somePointerWrapperError{"Hi!", nil})
    fmt.Println("pointer wrapper error trace ---\t", e2, "\t--- is traceable: ", e2IsE)
}


func main() {
    checkAtomicError()
    checkWrapperError() 
    checkPointerWrapperError()
}

//atomic error trace ---         whoa!: Hi!         --- is traceable:  true
//wrapper error trace ---        whoa!: Hi!: <nil>  --- is traceable:  true
//pointer wrapper error trace ---    whoa!: Hi!: <nil>  --- is traceable:  false

https://play.golang.org/p/-hSukZ-gii2

It seems that any difference in parameters, including in the wrapped error parameter, Err, will result in the type being unable to be found with errors.Is().

Acephalous answered 18/6, 2020 at 3:40 Comment(0)
O
14

The reason you are getting false while trying to find one error within another via errors.Is is because - while the two errors may have the same field values - they are two different memory pointers:

e  := &somePointerWrapperError{"Hi!", nil}
e2 := &somePointerWrapperError{"Hi!", nil} // e2 != e

ew := fmt.Errorf("whoa!: %w", e)

errors.Is(ew, e)  // true
errors.Is(ew, e2) // false - because `ew` wraps `e` not `e2`

So how to detect this "type" of error and get its value: use errors.As instead:

e := &somePointerWrapperError{"Hi!", nil}
e2 := fmt.Errorf("whoa!: %w", e)

var ev *somePointerWrapperError

if errors.As(e2, &ev) {
    fmt.Printf("%#v\n", ev) // &somePointerWrapperError{Msg:"Hi!", Err:error(nil)}
}

https://play.golang.org/p/CttKThLasXD

Occidental answered 18/6, 2020 at 3:59 Comment(0)
V
8

Remotely related, but maybe it helps someone: It took me some time to realize that errors.As(...) actually expects a double pointer to the target, while errors.Is(...) doesn't:

var _ error = (*CustomError)(nil) // ensure CustomError implements error

type CustomError struct {
    msg string
}

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

func main() {
    err := &CustomError{"Hello, world!"} // Methods return pointers to errors, allowing them to be nil
    
    var eval *CustomError

    as := errors.As(err, &eval) // yes, that's **CustomError
    asFaulty := errors.As(err, eval) // no compile error, so it wrongly seems okay
    is := errors.Is(err, eval) // that's just *CustomError

    fmt.Printf("as: %t, asFaulty: %t, is: %t", as, asFaulty, is) // as: true, asFaulty: false, is: true
}
Vaish answered 11/3, 2021 at 16:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.