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?
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))
}