package main
import (
"errors"
"fmt"
)
type myError struct{ err error }
func (e myError) Error() string { return e.err.Error() }
func new(msg string, args ...any) error {
return myError{fmt.Errorf(msg, args...)}
}
func (e myError) Unwrap() error { return e.err }
func (e myError) As(target any) bool { return errors.As(e.err, target) }
func (e myError) Is(target error) bool { return errors.Is(e.err, target) }
func isMyError(err error) bool {
target := new("")
return errors.Is(err, target)
}
func asMyError(err error) (error, bool) {
var target myError
ok := errors.As(err, &target)
return target, ok
}
func main() {
err := fmt.Errorf("wrap: %w", new("I am a myError"))
fmt.Println(isMyError(err))
fmt.Println(asMyError(err))
err = fmt.Errorf("wrap: %w", errors.New("I am not a myError"))
fmt.Println(isMyError(err))
fmt.Println(asMyError(err))
}
I expected
true
I am a myError true
false
I am not a myError false
but I got
false
I am a myError true
false
%!v(PANIC=Error method: runtime error: invalid memory address or nil pointer dereference) false
I tried to add
func (e myError) Unwrap() error { return e.err }
func (e myError) As(target any) bool { return errors.As(e.err, target) }
func (e myError) Is(target error) bool { return errors.Is(e.err, target) }
I tried
func asMyError(err error) (error, bool) {
target := &myError{} // was 'var target myError' before
ok := errors.As(err, &target)
return target, ok
}
I tried
func new(msg string, args ...any) error {
return &myError{fmt.Errorf(msg, args...)} // The change is the character '&'
}
but none of these changed anything. I also tried
func asMyError(err error) (error, bool) {
target := new("") // // was 'var target myError' or 'target := &myError{}' before
ok := errors.As(err, &target)
return target, ok
}
and then I got
false
wrap: I am a myError true
false
wrap: I am not a myError true
, which I guess makes sense but again does not solve my problem. I have a hard time to wrap my head this problem. Can you give me a hand?
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.
I think I understand errors in Go and the errors package much better, after reading Elias' answer and this go.dev blog.
Effectively, there seem to be two major paradigms for errors in Go:
A package defines/exports "sentinel" errors, which are returned from that package's functions. This is suitable for errors which don't require any additional data to be attached.
var (
ErrTimeout = errors.New("timeout")
ErrNotFound = errors.New("not found")
...
)
Errors returned from this package can be compared by value against the sentinel errors. In the past, this comparison was just done with ==, but in the age of error wrapping, you should use errors.Is to compare instead.
res, err := pkg.DoThing()
if errors.Is(err, pkg.ErrTimeout) {
...
}
A package defines/exports custom error types, which are returned from that package's functions. This is suitable for attaching additional data/context/info to an error.
type IOError struct {
filename string
exitCode int
action string
errorInfo string
}
func (e *IOError) Error() string {
return fmt.Sprintf("%s file %q: %s", e.action, e.filename, e.errorInfo)
}
Errors returned from this package should have their types compared against the exported error types. In the past, this would be done with a type assertion on the error, but in the age of error wrapping, you should use errors.As to compare instead.
res, err := pkg.DoThing()
ioError := &pkg.IOError{}
if errors.As(err, &ioError) {
os.Remove(ioError.filename)
...
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With