---
# System prepended metadata

title: Go's `errors.Is` and Custom Error Types

---

# 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
    ```
