go

Errors comparing in Golang


Today I ran into a problem when trying to implement custom errors. My service has 2 types of errors: regular for internal errors and user errors which handles user-related error. So I have struct for user errors with some meta data and function which processing errors. In this function I use wrapper on standard errors.As function. But it worked in a strange way: for some reason, it considered common errors to be user errors, too. Here is code snippet:

package main

import (
    "errors"
    "fmt"
)

type UserError struct {
    Message string
}

func (u *UserError) Error() string {
    return u.Message
}

func As(sourceError, targetError error) bool {
    return errors.As(sourceError, &targetError)
}

func AsV2(sourceError error, targetError interface{}) bool {
    return errors.As(sourceError, &targetError)
}

func IsUserError(err error) bool {
    var userError *UserError
    return errors.As(err, &userError)
}

func main() {
    userError := errors.New("test Error")
    var emptyError *UserError

    fmt.Println(As(userError, emptyError))
    fmt.Println(AsV2(userError, emptyError))
    
    fmt.Println(IsUserError(userError))
}

Here I am testing functions (As and AsV2) that takes two errors and compare them. The only difference that second function accepts the interface of target error instead of error type. In function IsUserError I manually creating UserError pointer and giving to errors.As function. But on output this programm I getting this:

true
true 
false

So my question why in first two cases errors.As saying that it's errors of the same type but only in the third case it gives the correct answer? Am I misunderstanding how interfaces work in Go?


Solution

  • It isn't. In the first two cases, it's correctly reporting that the error is error and interface{}, respectively. The unnecessary wrapper functions you've added are creating an indirection that makes it more confusing, but if you call:

    var emptyError *UserError
    fmt.Println(errors.As(userError, emptyError))
    

    you get your expected result. IsUserError works as expected because it passes the correct type to errors.As. I've cleaned up your code to work correctly (Error taking a non-pointer receiver and not leaving emptyError as nil) and you can see it in action here: https://go.dev/play/p/c5EPB5IGeD5

    type UserError struct {
        Message string
    }
    
    func (u UserError) Error() string {
        return u.Message
    }
    
    func main() {
        userError := errors.New("test Error")
        var emptyError UserError
    
        fmt.Println(errors.As(userError, &emptyError))
    }