# Go's `errors.Is` and Custom Error Types [TOC] ## 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]). [1]: https://github.com/golang/go/blob/b07b20fbb591ac77158e1089299ce5acad71ffde/src/errors/wrap.go#L55 "Is(err, target error) bool" 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`](https://github.com/golang/go/blob/b07b20fbb591ac77158e1089299ce5acad71ffde/src/io/fs/fs.go#L146): ```go errors.Is(err, fs.ErrExist) ``` But custom error types often include struct fields, such as: ```go type CertPendingError struct { CertPath string Code int Descr string } ``` The contents of `CertPath` and `Err` is variable; for example, it could be: ```go 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](#Go-Custom-Errors-Concrete-or-Pointers). So, to make `errors.Is` work with this particular error, you would have to match it exactly: ```go 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: ```go errors.Is(err, CertPendingError{}) ``` As a workaround, people fall back to using `errors.As` or a type assertion: ```go errors.As(err, &CertPendingError{}) ``` Or they default to the old way of comparing errors: ```go _, 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: ```go 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: ```go 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: ```go 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: ```go 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: ```text 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: ```go 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: ```go 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: ```go 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: ```go 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: ```go 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: ```go func (e *Err) Error() string ```