Skip to content
Lesson 5 of 7

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:

go
*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.

go
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:

go
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. Every Is and As below it answers no.
  • An Is method that ignores target. A code error then claims to be not found too.
  • A class check off by one. > 400 rejects 400; >= 400 accepts 500.
  • Taking As's true as the behaviour. A 404 has a Timeout method too, so it prints yes while 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.

Up nextTesting and BenchmarkingErrors, Testing, and Performance

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…