Applying `errors.Is` and `errors.As` on custom made struct errors
Asked Answered
A

2

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

Aubergine answered 19/5, 2022 at 15:40 Comment(6)
having a function called new looks off. Aslo: implementing the Is and As methods makes no sense. The errors.Is and errors.As functions are functions from the errors package themselves. Looking at the source code, the main thing is your error type has to implement an Unwrap method returning your custom typeRobb
new, isMyError and asMyError 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 this Unwrap function, could clarify that please?Aubergine
I’ve found this an interesting look at errors in Go. dave.cheney.net/2016/04/27/… In this case, Wrap() takes the error and a custom string.Plata
@Aubergine wrote up an answer. The thing is that I simply haven't use errors.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 of errors.Is, though, are easy to find, like hereRobb
errors are not so much about communicating information through types (that's more of an exception thing). Errors mainly communicate through plain old strings. Strings that can be assigned to exported variables indicating the exact problem (just like error codes). It looks a bit like you're looking for a more exception style error handling system/type hereRobb
To me, For Sentinel errors - match with errors.Is, and for Structured errors - match with errors.As, refer to https://mcmap.net/q/193830/-go-checking-for-the-type-of-a-custom-errorJudie
R
12

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.

Robb answered 19/5, 2022 at 16:36 Comment(0)
M
7

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:

  1. A package defines/exports constant "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.

    const (
      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) {
      ...
    }
    
  2. 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)
      ...
    }
    
Meurer answered 17/8, 2023 at 6:26 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.