skip to content

Implementing Is and As

Your own error type can answer errors.Is and errors.As by implementing Is and As methods, which is how one value matches several sentinels. Few candidates know those hooks exist.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

What does adding an Is(target error) bool method to your own error type change about errors.Is?

level: juniorimportance: must knowfreq 42%

answer

  1. equivalence, not identity
  2. == is tried first and fails for values with fields
  3. your type gets a callback at its own link
  4. shallow only: never unwrap inside it
  5. syscall.Errno does this for portable file sentinels

basics

~20 s

An Is method lets errors.Is treat your error as equivalent to a target it does not equal by value. While searching, errors.Is calls your Is method at your link and accepts a true answer as a match.

solid answer

~40 s

By default `errors.Is` matches by identity: at each link it compares the error with the target using `==`. That fails for an error type that carries fields, because every instance is a distinct value. Declaring `func (e *MyErr) Is(target error) bool` gives your type a say: when the `==` comparison fails, `errors.Is` type-asserts the link to `interface{ Is(error) bool }` and calls it, and a `true` return ends the search successfully. That is how a status-carrying error can satisfy `errors.Is(err, ErrNotFound)` without ever holding `ErrNotFound`; `syscall.Errno.Is` does exactly this in the standard library, mapping raw errno values onto `fs.ErrNotExist` and friends. The documented contract is that the method compares only shallowly — it must not unwrap either side, because the search is already doing that.

code

go · 17 lines
go
var ErrNotFound = errors.New("not found")

type StatusError struct {
	Status int
	Op     string
}

func (e *StatusError) Error() string {
	return fmt.Sprintf("%s: upstream status %d", e.Op, e.Status)
}

// Is is consulted by errors.Is at this link.
func (e *StatusError) Is(target error) bool {
	return target == ErrNotFound && e.Status == 404
}

// errors.Is(&StatusError{Status: 404, Op: "fetch"}, ErrNotFound) is true

go deeper

for a junior

Be ready to say what errors.Is does by default (compares each error in the chain with the target) and that a type can add an Is method to be treated as equal to a sentinel it is not. Know the method signature exactly.

for a middle

Explain the mechanics: the anonymous interface assertion, that the method is consulted at that link before the search moves on, that a true ends the search, and that method sets decide whether a value or only a pointer exposes it.

for a senior

Show the judgment: which equivalences you are willing to publish and keep stable, why an over-broad Is silently breaks every caller's branching, and how you test the negative case as well as the positive one.

for a principal

Frame it as API surface. Once callers match on a sentinel your Is method maps onto, that mapping is a contract you cannot change without breaking their control flow, so decide deliberately which foreign failure vocabulary you adopt into your own.

## The default rule: identity `errors.Is(err, target)` answers the question "is this failure, or anything it wraps, the specific error `target`?". Its default test is Go's `==` operator applied to two `error` interface values, which compares the dynamic type and the dynamic value. That works perfectly for sentinel errors — package-level values such as `var ErrNotFound = errors.New("not found")` — because there is exactly one of them and every reference is the same pointer. It stops working the moment your error carries data. If your package returns `&StatusError{Status: 404, Op: "fetch user"}`, every call site builds a fresh value, so `==` against anything is false. Callers still want to write the natural check: "was this a not-found?". ## The optional Is method The `errors` package defines an optional, unnamed interface for this: ```go interface{ Is(error) bool } ``` If a link in the chain implements it, `errors.Is` calls it with the target after the `==` comparison at that link has failed. A `true` return means "treat me as equal to that target" and the search returns true immediately. A `false` return means "no claim", and the search carries on by unwrapping to the next link. So the method is a declaration of *equivalence*, not of identity. Your `*StatusError` never becomes `ErrNotFound`; it merely asserts, for the purpose of matching, that a 404 frame counts as one. ```go var ErrNotFound = errors.New("not found") func (e *StatusError) Is(target error) bool { return target == ErrNotFound && e.Status == 404 } ``` With that method, `errors.Is(&StatusError{Status: 404}, ErrNotFound)` is true, and callers of your package can branch on a sentinel you export while you keep returning a rich value with fields they can also extract. ## Where the method must live The method is found by a type assertion on the *dynamic* value in the chain. Method sets therefore decide whether it is visible: a method declared with receiver `*StatusError` belongs to `*StatusError` only, so it is consulted when the chain holds `&StatusError{...}` and is invisible if the chain holds a bare `StatusError` value. Return your errors as pointers and declare `Is` on the pointer receiver, consistently with the type's other methods, and the question does not arise. ## The contract, and why it exists The package documentation is explicit: an `Is` method should compare the receiver and the target *shallowly*, and must not call `Unwrap` on either one. The reason is that `errors.Is` is already the thing that walks: it will visit every link and offer the target to each one. If your method unwraps as well, you get quadratic work on deep chains, you can produce surprising matches for links that were never supposed to be consulted at that point, and a self-referential chain turns into an infinite loop. The method should also be pure and cheap — it is called once per link per lookup, and callers may call `errors.Is` in a hot retry loop. A single method may claim equivalence with several targets, which is a common and legitimate shape: ```go func (e *StatusError) Is(target error) bool { switch target { case ErrNotFound: return e.Status == 404 case ErrRateLimited: return e.Status == 429 } return false } ``` ## The standard library's own example `syscall.Errno` is a plain integer type, and platform errno numbers are not portable. Its `Is` method maps them onto the portable sentinels — `ENOENT` onto the value behind `fs.ErrNotExist`, permission errors onto `fs.ErrPermission`, and so on — which is why `errors.Is(err, fs.ErrNotExist)` works across operating systems even though no code ever wrapped one sentinel in the other. That is the canonical use: a foreign or numeric failure vocabulary being adapted onto the vocabulary your callers already know. ## When not to write one Do not add `Is` to make matching *convenient* when identity already works — a sentinel needs nothing. Do not add it to express "this error is roughly like that one" if the equivalence is not something you are willing to keep stable, because the moment a caller writes `errors.Is(err, ErrNotFound)` against your package, the mapping is part of your public contract and changing it silently changes their control flow. And never make it broad — an `Is` that returns true for any target turns every `errors.Is` question on that chain into a yes. ## Testing it Assert both directions: that a value which should match does, and that a near-miss (a 500 frame against `ErrNotFound`) does not. The second half is the one that catches an over-broad method, and it is the half people skip.

  • Your Is method is declared on the pointer receiver, but the chain holds a bare StatusError value. Does errors.Is still call it?
    No. `errors.Is` finds the method by asserting the dynamic value in the chain to `interface{ Is(error) bool }`, and a method with receiver `*StatusError` is in the method set of `*StatusError` only. A `StatusError` value therefore does not implement the interface, the method is skipped silently, and matching falls back to `==`. Return the error as a pointer, or declare the method on the value receiver — consistently with the type's other methods.
  • Why does the errors package say an Is method must not call Unwrap on either error?
    Because `errors.Is` already unwraps: it offers the target to every link in turn. Unwrapping inside the method repeats that walk at each link, which is quadratic on a deep chain, can report matches for links the search had not reached yet, and turns any accidental cycle into a hang. Keep the method a shallow, cheap, side-effect-free comparison of the receiver's own fields against the target.
  • Can one Is method make an error match several different sentinels?
    Yes, and it is the normal shape when one type covers a family of outcomes. Switch on the target and return the condition for each: `case ErrNotFound: return e.Status == 404`, `case ErrRateLimited: return e.Status == 429`, default `false`. Every mapping you add becomes part of your package's contract, so keep the list explicit rather than answering true for anything unrecognised.

A sentinel match is checking passport numbers; an Is method is a letter from your embassy saying this person counts as a citizen for this purpose.

saying these in an interview costs you the question

  • Thinks errors.Is only ever compares with == and never calls a method
  • Writes an Is method that unwraps or calls errors.Is on its own cause
  • Returns true for any target, so every match question answers yes
  • Declares Is on the pointer receiver but returns the error by value
  • Assumes an error type with fields matches sentinels automatically
open as a page

When does an error type need its own As(target any) bool method, and what must that method do?

level: middleimportance: should knowfreq 30%

basics

~20 s

You need one only when callers must extract a type the chain does not contain, such as a view synthesised from your fields. The method must recognise the pointer it is handed, assign through it, then return true.

open as a page

In what order does errors.Is try ==, a link's own Is method, and unwrapping to the next error?

level: middleimportance: should knowfreq 50%

basics

~20 s

At each link errors.Is first compares that link with the target using == when the target's type is comparable, then calls the link's Is(error) bool method if it has one, and only then unwraps to the next link and repeats.

open as a page

errors.As returns true but the caller's target is zero-valued. How do you diagnose that in a proxy?

level: seniorimportance: nice to knowfreq 36%

basics

~20 s

Some link's custom As method returned true without assigning through the target pointer. Because errors.As trusts that true and stops walking, the real cause below is never reached. Dump the chain and read the first link that declares As.

open as a page