Try   HackMD

Go's errors.Is and Custom Error Types

Problem

Go's errors.As works as expected with custom error types. You don't need to do anything special to make it work. But errors.Is is different: by default, it uses == to compare the error with the target (1).

This is OK for simple custom types that don't have struct fields, and it is particularly well suited to what Go calls "sentinel errors". A sentinel error is a well-known error value that you can use to know what an error is.

An example of sentinel error is fs.ErrExist:

errors.Is(err, fs.ErrExist)

But custom error types often include struct fields, such as:

type CertPendingError struct {
    CertPath string
    Code     int
    Descr    string
}

The contents of CertPath and Err is variable; for example, it could be:

err := CertPendingError{CertPath: `\VED\Policy\cert`, Code: 400, Descr: "Certificate does not exist"}

NOTE: If you are confused and think it should be &CertPendingError{...} instead, you can read the section below.

So, to make errors.Is work with this particular error, you would have to match it exactly:

errors.Is(err, CertPendingError{CertPath: `\VED\Policy\cert`, Code: 400, Descr: "Certificate does not exist"})

It doesn't make sense What you really want is to do:

errors.Is(err, CertPendingError{})

As a workaround, people fall back to using errors.As or a type assertion:

errors.As(err, &CertPendingError{})

Or they default to the old way of comparing errors:

_, ok := err.(CertPendingError)

Solution

Go's errors package allows us to "tweak" how errors.Is works for custom types that have struct fields. Since CertPath and Err are changing for each error, we just ignore those fields:

func (e CertPendingError) Is(target error) bool {
    _, ok := target.(CertPendingError)
    return ok
}

This lets us write:

```go
errors.Is(err, &CertificatePendingError{})

But don't you want to match on the Code too? It would allow you to create specific sentinel errors for each error code. Let's do it:

const (
    ErrCertPending = CertPendingError{Code: 400}
    ErrCertFailure = CertPendingError{Code: 600}
)

func (e CertPendingError) Is(target error) bool {
    if target == nil {
        return false
    }
    t, ok := target.(CertPendingError)
    return ok && e.Code == t.Code
}

This lets us write:

errors.Is(err, ErrCertPending)

Go Custom Errors: Concrete or Pointers?

Go errors can be concrete struct values; it all depends on how you implemented the Error() string func. It is easy to get confused by the error returned by the Go compiler when trying to use a struct value as an error. For example, consider the following custom error:

type Err struct{}
func (e *Err) Error() string {  // ⚠️ Pointer receiver (e *Err)
    return "this is an Err"
}
func Func() error {
    return Err{}                // ❌ Can't return a concrete value.
}

The compiler will return:

main.go:12:9: cannot use Err{} (value of type Err) as error value in return statement: Err does not implement error (method Error has pointer receiver)

This error misleads you in thinking that errors must be pointers. What it actually means is that the Error() string method is implemented over *Err instead of Err. The following code works:

type Err struct{}
func (e Err) Error() string {   // ✅ Concrete receiver (e Err)
    return "this is an Err"
}
func Func() error {
    return Err{}                // ✅ Concrete value
}

That's because concrete values can implement interfaces. Another reason that may lead you to think that errors must be pointers is the fact that interface values can be nil, making you believe that an error needs to be a pointer. This illusion comes from the fact that an interface is implemented as a pair: a value and a type. For a concrete type implementing an interface, the pair only has two possible combinations:

Value Type
nil none
Err{} Err

The pair (value=nil, type=Err) is impossible. So, with custom error based on concrete structs, you cannot return a typed nil value. The compiler will say:

err = (Err)(nil) // ❌ cannot convert nil to type Err

But are typed pointers useful for custom errors? The answer is no! Let's take the following pointer-based custom error:

type Err struct{}

func (e *Err) Error() string {
    return "this is an Err"
}

The usual err == nil won't work with this typed nil value:

func main() {
    var err error
    err = (*Err)(nil)         // ✅
    fmt.Println(err == nil)   // ❌ The nil assertion no longer works!
}

To recap:

  1. Most of the time, you will want to use a concrete receiver for your custom errors:

    ​​​​func (e Err) Error() string
    
  2. In some cases, you may need to use a pointer receiver, for example if some value contained is shared or really big:

    ​​​​func (e *Err) Error() string