Rrors.is Do With A Wrapped Chain
Fast recall
Quick revision
Core answer
Practical understanding of Fmt Errorf.
Repeating a definition without saying when to use it.
Give one trade-off and say how you would test it.
Interview-ready guide
Interview answer
errors.Is is the chain-aware equality check:
var ErrNotFound = errors.New("not found")
err := fmt.Errorf("get user: %w", ErrNotFound)
errors.Is(err, ErrNotFound) // true — found through the wrap
err == ErrNotFound // false — they're different valuesThe walk: Is takes the error and follows Unwrap edges — each %w is one edge — comparing each node against the target with == (or the target's Is method, if it has one). The sentinel can sit any number of wraps deep; Is finds it because the chain is traversable data.
This is why Is replaced raw == for sentinels: == sees only the top value (the outermost wrap — not equal), while Is sees the whole causal history. A caller that uses == breaks the moment any intermediate layer wraps; a caller that uses Is survives arbitrary wrapping — which is exactly the compat contract between layers that don't know each other.
The As variant does the same walk for types: errors.As(err, &myErrTypedPointer) finds the first node assignable to the type and copies it out. Is for identities, As for types with fields — both powered by the same chain.
Rrors.is Do With A Wrapped Chain
Direct Answer
Walks the Unwrap chain comparing each error to the target. fmt.Errorf %w links the chain; Is finds the sentinel anywhere inside.
Deep Dive & Architecture
Is is tree-walking identity comparison, and each clause deserves its own look.
IDENTITY: Is's default comparison is ==. errors.Is(err, target) walks the chain testing each node == target. For sentinel errors (single package-level values), == is exact: the pointer IS the sentinel's pointer. Is supports a second comparison channel: if the TARGET implements Is(error) bool, that method is consulted — this is how error types match as a CLASS (os.ErrNotExist matches various filesystem errors that aren't the same value but mean the same thing — their Is method says so).
THE WALK: Is follows Unwrap() error edges (linear chains — each fmt.Errorf %w adds one) AND Unwrap() []error edges (Join branches). The walk is depth-first; cyclic chains cause infinite loops (the errors package documents this — don't build cycles).
What the caller's code looks like, and why it composes:
if errors.Is(err, context.Canceled) {
return // client gave up — don't log as an error
}
if errors.Is(err, os.ErrNotExist) {
return defaults() // the whole filesystem-family 'not exists' class
}
Neither check knows how many layers wrapped the error — that's the compatibility contract. Middle layers wrap freely with %w; boundary layers branch on identity through any wrapping depth. The sentinels stay matchable across package versions, refactorings of call depth, and third-party libraries in between. That indirection-proof matching is what made sentinel errors viable again post-1.13 — before Is, every == broke on the first wrap.
Common Mistakes & Pitfalls
Repeating a definition without saying when to use it.
Check your understanding
Answer these without looking back at the article.
- What is Fmt Errorf, and when would you use it?
- When would you use Fmt Errorf in a real project?
- How would you compare Fmt Errorf with an alternative?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.