Step 1 of 7 · Reading · ~4 min
Learn
Errors, Testing, and Performance
Error Chains: Unwrap, Is and As Under the Hood
You will write the types that errors.Is and errors.As walk: a wrapper with Unwrap, an Is method that lets a family of errors match one sentinel, and a lookup by behaviour instead of by type.
In Errors you wrapped with %w and called errors.Is and errors.As; here you write what they walk.
Unwrap: the chain is a linked list
An error wraps another when it has an Unwrap() error method. %w builds such a value for you; your own types add the method by hand, as os.PathError does. errors.Unwrap(err) peels exactly one layer and returns nil when there is no Unwrap, so walking the chain is a loop:
*fmt.wrapError checkpoint: save a.dat: disk full
*main.SaveError save a.dat: disk full
*errors.errorString disk full
true
Without the Unwrap method the loop ends after the *main.SaveError line and errors.Is prints false. The message still says "disk full", but the chain is cut and nothing warns you.
Is: one sentinel for a family
errors.Is(err, target) visits each error in the chain. At each one it checks err == target, then calls the error's own Is(target error) bool method if it has one, then unwraps and repeats. So a custom Is answers for its own layer only; it never unwraps.
true false false
Drop target == ErrKilled && and the last value becomes true: every question gets yes.
As: match a type or a behaviour
errors.As(err, &target) walks the same way and stops at the first error assignable to the type of target, which may be a concrete type or an interface. With an interface it matches anything that has the methods:
true false
As matches the method set, not the method's answer. The first true only says the chain contains something to ask; the second value is the answer, and it is no.
As panics at run time (go run does not run go vet) when the target is not a non-nil pointer: As(r, x) instead of As(r, &x) gives errors: target must be a non-nil pointer.
What %w promises
%w makes the cause part of your API: callers will write errors.Is(err, ErrDiskFull), and you cannot replace the disk layer without breaking them. %v keeps the text and cuts the chain: errors.Is(fmt.Errorf("get: %v", ErrDiskFull), ErrDiskFull) is false. Wrap when callers may act on the cause; use %v when it is an implementation detail. Add context once per layer.
Two traps. Wrapping nil does not give nil: fmt.Errorf("load: %w", err) with err == nil is a non-nil error reading load: %!w(<nil>), so guard it. And an error value holding a slice is not comparable: for type ListError struct{ Items []string }, errors.Is(a, a) is false for the same value. Use a pointer.
Common mistakes
- A wrapper without
Unwrap. EveryIsandAsbelow it answers no. - An
Ismethod that ignorestarget. A code error then claims to benot foundtoo. - A class check off by one.
> 400rejects 400;>= 400accepts 500. - Taking
As'strueas the behaviour. A 404 has aTimeoutmethod too, so it printsyeswhile it answers false.
Your exercise
Build an error chain from a line of text and query it. The Unwrap, Is and Timeout methods and four helpers are TODO; the likeliest mistake is the last one above.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…