In what order does errors.Is try ==, a link's own Is method, and unwrapping to the next error?
answer
- three steps, then descend
- identity, then the method, then the next link
- the target's comparability is checked once, up front
- a true ends the walk; a false only means no claim
- the outer link answers before its cause is ever asked
basics
~20 sAt 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.
solid answer
~50 s`errors.Is` is a loop over the chain, and at every link it tries three things in a fixed order. First `==` against the target, but only when the target's dynamic type is comparable — it checks that with reflection up front, so a struct target containing a slice or a map does not panic, it simply skips the comparison step. Second, if the link implements `interface{ Is(error) bool }`, it calls that method with the target; a `true` return ends the whole search successfully. Third, it unwraps to the next link and repeats. The search ends with `false` when a link has no unwrap method or unwrapping yields nil. The order matters in practice: a link's `Is` fires before anything deeper is ever consulted, so an over-broad `Is` near the top can answer for causes it knows nothing about.
code
go · 11 linestype wrapErr struct {
msg string
err error
}
func (w *wrapErr) Error() string { return w.msg + ": " + w.err.Error() }
func (w *wrapErr) Unwrap() error { return w.err }
func (w *wrapErr) Is(target error) bool {
return target == ErrRetryable && w.msg == "upstream busy"
}go deeper
Be ready to say that errors.Is looks at every error in the chain, not just the outermost one, and that it stops as soon as something matches. Knowing there is an order at all is the starting point.
State the three steps at each link in order and what ends the search. An interviewer expects you to know that a link's Is method runs before the search descends, and that a false return does not stop the walk.
Use the order to explain a real misdiagnosis: an outer type's Is answering for a cause it never inspected. Show how you would confirm which link produced the match rather than guessing.
Own the consequence for a shared package: the outermost error type in a chain has first refusal on every match question, so whoever owns that type effectively owns the failure vocabulary every caller branches on.
## The shape of the search `errors.Is(err, target)` is not a single comparison; it is a walk. Conceptually: ``` for each link, starting at err: 1. if the target's type is comparable and link == target -> true 2. if the link has an Is(error) bool method and it returns true -> true 3. move to the next link; if there is none -> false ``` Every one of those three steps is worth understanding, because each has a consequence people meet in real code. ## Step 0: the nil short-circuit Before the loop starts, a nil `err` or a nil `target` collapses to a plain `err == target`. So `errors.Is(nil, nil)` is true, and `errors.Is(nil, ErrNotFound)` is false, with no methods called and nothing walked. This matters when a helper passes a target it computed and did not check. ## Step 1: the comparability check `==` on two interface values panics at run time if the dynamic type is not comparable — a struct with a slice field, for example. `errors.Is` avoids that by asking, once, whether the *target's* type is comparable, using reflection, and skipping the comparison step for the whole walk if it is not. This is a genuinely useful thing to know: passing an uncomparable error value as a target does not blow up, it just means identity matching is off and only `Is` methods can produce a match. It also explains why the comparability question is asked about the target rather than about each link. ## Step 2: the Is method at this link The link is asserted to the anonymous interface `interface{ Is(error) bool }`. If it satisfies it, the method is called with the target, and `true` returns immediately from the entire search. A `false` return is not a rejection of the chain — it means "this link makes no claim", and the walk continues. Two consequences follow from the method being tried *at this link, before descending*: - Your own error type gets to answer before its cause is ever consulted. If your `Is` says true, whatever you wrapped is never asked. That is the whole point when you are adapting a foreign failure vocabulary — and it is the hazard when the method is written too broadly. - The method is called once per link, per `errors.Is` call. On a deep chain in a hot loop, an expensive `Is` costs more than people expect, which is another reason the documented contract says the comparison must be shallow and must not itself unwrap. ## Step 3: descending The walk continues by asking the link for its cause. If the link does not offer one, or the cause is nil, the search ends and `errors.Is` returns false. Nothing about "having fields" or "being a different type" stops the descent early — only the absence of a next link does. ## Why the order is the interesting part Suppose a proxy returns an error whose outer link has an `Is` method that maps upstream frames onto your package's sentinels, and whose cause is a transport failure that itself matches a different sentinel. Because step 2 runs at the outer link before step 3 descends, the outer type's opinion wins whenever it says true. Written narrowly — "true only when the target is `ErrRateLimited` and my status is 429" — that is exactly what you want. Written as "true whenever the target is one of my sentinels", it starts claiming failures it never inspected, and callers get a match that is wrong in a way no stack trace shows. The reverse mistake is expecting your `Is` to be able to *prevent* a match. It cannot. Step 1 has already run, and a `false` from your method does not veto anything: if a deeper link matches, `errors.Is` still returns true. An `Is` method is a way to say yes, never a way to say no. ## What to check when a match surprises you Walk the chain yourself and print each link's dynamic type. Then, for each type in the chain, ask whether it declares `Is` — a match you cannot explain by identity is coming from one of them, and the one nearest the top gets first refusal. Reading the outermost type's `Is` first is almost always the fastest route to the answer. ## Testing the order A good unit test for a type with an `Is` method builds a chain where a *deeper* link would also match, and asserts the outcome you intend. That is the test that fails when someone later widens the outer method, and it is the one that documents which link is supposed to be authoritative.
- If your Is method returns false, can errors.Is still return true for that same target?Yes. A `false` means only that this link makes no claim; the search continues. If any deeper link equals the target or has an `Is` that says true, `errors.Is` returns true. There is no veto: an `Is` method can only add matches, never suppress one. If you need to stop a deeper cause from being visible to callers, do not expose it in the chain at all.
- What happens if the target passed to errors.Is is a struct error type containing a slice field?Nothing panics. `errors.Is` checks the target type's comparability once before walking and simply skips the `==` step when it is not comparable. The walk still runs, so a match can only come from a link's `Is` method. If you export an uncomparable error type, callers matching on a value of it will silently never match by identity — a reason to prefer comparable sentinels or an `Is` method.
- How expensive is errors.Is on a deep chain?It is linear in the chain length, with an interface type assertion and possibly an `Is` call at every link. That is cheap when the methods are shallow field comparisons, which is what the documented contract asks for. It becomes noticeable when an `Is` method does real work — string formatting, map lookups, or its own unwrapping — and code calls `errors.Is` per request or inside a retry loop.
saying these in an interview costs you the question
- Thinks unwrapping happens before the current link is tested
- Believes a false from an Is method blocks a deeper match
- Expects errors.Is to panic on an uncomparable target
- Assumes the Is method is only consulted after the whole chain fails
- Thinks errors.Is compares error messages when types differ