So the point of errors.Is
and errors.As
is that errors can be wrapped, and these functions allow you to extract what the underlying cause of a given error was. This essentially relies on certain errors having specific error values. Take this as an example:
const (
ConnectionFailed = "connection error"
ConnectionTimedOut = "connection timed out"
)
type PkgErr struct {
Msg string
}
func (p PkgErr) Error() string {
return p.Msg
}
func getError(msg string) error {
return PkgErr{
Msg: msg,
}
}
func DoStuff() (bool, error) {
// do some stuff
err := getError(ConnectionFailed)
return false, fmt.Errorf("unable to do stuff: %w", err)
}
Then, in the caller, you can do something like this:
_, err := pkg.DoStuff()
var pErr pkg.PkgErr
if errors.As(err, &pErr) {
fmt.Printf("DoStuff failed with error %s, underlying error is: %s\n", err, pErr)
}
Or, if you only want to handle connection timeouts, but connection errors should instantly fail, you could do something like this:
accept := pkg.PkgErr{
Msg: pkg.ConnectionTimedOut,
}
if err := pkg.DoStuff(); err != nil {
if !errors.Is(err, accept) {
panic("Fatal: " + err.Error())
}
// handle timeout
}
There is, essentially, nothing you need to implement for the unwrap/is/as part. The idea is that you get a "generic" error back, and you want to unwrap the underlying error values that you know about, and you can/want to handle. If anything, at this point, the custom error type is more of a nuisance than an added value. The common way of using this wrapping/errors.Is
thing is by just having your errors as variables:
var (
ErrConnectionFailed = errors.New("connection error")
ErrConnectionTimedOut = errors.New("connection timed out")
)
// then return something like this:
return fmt.Errorf("failed to do X: %w", ErrConnectionFailed)
Then in the caller, you can determine why something went wrong by doing:
if errors.Is(err, pkg.ErrConnectionFailed) {
panic("connection is borked")
} else if errors.Is(err, pkg.ErrConnectionTimedOut) {
// handle connection time-out, perhaps retry...
}
An example of how this is used can be found in the SQL packages. The driver package has an error variable defined like driver.ErrBadCon
, but errors from DB connections can come from various places, so when interacting with a resource like this, you can quickly work out what went wrong by doing something like:
if err := foo.DoStuff(); err != nil {
if errors.Is(err, driver.ErrBadCon) {
panic("bad connection")
}
}
I myself haven't really used the errors.As
all that much. IMO, it feels a bit wrong to return an error, and pass it further up the call stack to be handled depending on what the error exactly is, or even: extract an underlying error (often removing data), to pass it back up. I suppose it could be used in cases where error messages could contain sensitive information you don't want to send back to a client or something:
// dealing with credentials:
var ErrInvalidData = errors.New("data invalid")
type SanitizedErr struct {
e error
}
func (s SanitizedErr) Error() string { return s.e.Error() }
func Authenticate(user, pass string) error {
// do stuff
if !valid {
return fmt.Errorf("user %s, pass %s invalid: %w", user, pass, SanitizedErr{
e: ErrInvalidData,
})
}
}
Now, if this function returns an error, to prevent the user/pass data to be logged or sent back in any way shape or form, you can extract the generic error message by doing this:
var sanitized pkg.SanitizedErr
_ = errors.As(err, &sanitized)
// return error
return sanitized
All in all though, this has been a part of the language for quite some time, and I've not seen it used all that much. If you want your custom error types to implement an Unwrap
function of sorts, though, the way to do this is really quite easy. Taking this sanitized error type as an example:
func (s SanitizedErr) Unwrap() error {
return s.e
}
That's all. The thing to keep in mind is that, at first glance, the Is
and As
functions work recursively, so the more custom types that you use that implement this Unwrap
function, the longer the actual unwrapping will take. That's not even accounting for situations where you might end up with something like this:
boom := SanitizedErr{}
boom.e = boom
Now the Unwrap
method will simply return the same error over and over again, which is just a recipe for disaster. The value you get from this is, IMO, quite minimal anyway.
new
looks off. Aslo: implementing theIs
andAs
methods makes no sense. Theerrors.Is
anderrors.As
functions are functions from the errors package themselves. Looking at the source code, the main thing is your error type has to implement anUnwrap
method returning your custom type – Robbnew
,isMyError
andasMyError
were just here to highlight what were my misunderstanding. I will never use such function in an internal errors package of mine. I do not understand how I should implement thisUnwrap
function, could clarify that please? – Aubergineerrors.As
at all, and bar some very specific use-cases, I don 't really see the reason why I would use something like that. Not unless I decided to rework all of my error types (and I don't use a lot of them anyway) and wrap them in a type that filters out sensitive information or something. Good examples oferrors.Is
, though, are easy to find, like here – Robberrors.Is
, and for Structured errors - match witherrors.As
, refer to https://mcmap.net/q/193830/-go-checking-for-the-type-of-a-custom-error – Judie