Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Elegant way to check error is wrapped in Go

We have one function that returns errors one of them is wrapped by errors.Wrap(), the others are not.

var ErrTest1 = errors.New("error test 1")
var ErrTest2 = errors.New("error test 2")
var ErrRPC = errors.New("error rpc")

func rpcCall() error {
    return ErrRPC
}

func testErrWrap(a int) error {
    if a == 1 {
        return ErrTest1
    } else if a == 2 {
        return ErrTest2
    } else {
        err := rpcCall()
        if err != nil {
            return errors.Wrap(ErrRPC, "call rpc err")
        }
    }
    return nil
}

We have two solutions, one is

    err := testErrWrap(3)

    if errors.Unwrap(err) != nil {
        fmt.Println(errors.Unwrap(err))
    }

the other is

    err := testErrWrap(3)

    if !errors.Is(err, ErrTest2) && !errors.Is(err, ErrTest1) {
        tErr := errors.Unwrap(err)
        fmt.Println(tErr)
    }

We want to know the elegant way to distinguish errors are wrapped or not in Go?

like image 257
zangw Avatar asked Oct 24 '25 04:10

zangw


2 Answers

In most cases we're looking to find out if we got a specific type of error in order to do some special logic to handle it. I'd argue there are better ways to do this than looking at whether the error was wrapped or not.

In this case I'd propose using custom error types and using errors.Is or errors.As

First let's create some code that has a custom error type:

type ErrRPC struct { 
   retryPort int
}

func (e ErrRPC) Error() string {
    return "Oh no, an rpc error!"
}

func testErrWrap(a int) error {
    switch a {
    case 1:
        return errors.New("errors test 1")
    case 2:
        return errors.New("errors test 1")
    default:
        err := rpcCall(9000)
        return fmt.Errorf("errors test: %w", err) // Wraps the ErrRPC
    }
}

func rpcCall(port int) error {
    return ErrRPC{retryPort: port + 1}
}

If we're only interested in going down a different code path when we get a specific error type I'd go with errors.Is

func main() {
    for i := 1; i <= 3; i++ {
        if err := testErrWrap(i); err != nil {
            if errors.Is(err, ErrRPC{}) {
                println("rpc error")
            } else {
                println("regular error")
            }
        }
    }
}

If we need to use a property of the error value for something, errors.As comes in handy.

func main() {
    for i := 1; i <= 3; i++ {
        if err := testErrWrap(i); err != nil {
            var rpcErr ErrRPC
            if errors.As(err, &rpcErr) {
                fmt.Printf("rpc error, retrying on port: %d", rpcErr.retryPort)
            } else {
                println("regular error")
            }
        }
    }
}
like image 125
Pierre Fouilloux Avatar answered Oct 25 '25 19:10

Pierre Fouilloux


Another method from puellanivis

Also consider testing for behavior:

err := testErrWrap(3)

var wrappedErr interface { Unwrap() error }
if errors.As(err, &wrappedErr) {
    fmt.Println(errors.Unwrap(err))
}

But also of note, errors.Wrap(…) double wraps your error: one WithMessage and one WithStack. So using errors.Unwrap(err) on the error from errors.Wrap(ErrRPC, "call rpc err") will not give you ErrRPC.

like image 35
zangw Avatar answered Oct 25 '25 19:10

zangw



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!