Wrapping and Sentinel Errors
An error born deep in a call stack — "no such file" — is short on context by the time it reaches whoever logs it. The idiom is for each layer to wrap the error with what it was doing:
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("loading config: %w", err)
}
// ...
}
The %w verb (w for wrap) works like %v — the underlying message is
embedded — but it also records the original error inside the new one, forming
a chain. The final message reads like a story: loading config: open app.conf: no such file or directory. Each hop added its clause and kept the
cause.
Sentinel errors
Sometimes callers need to react to a specific failure, not just "an error". The pattern is a package-level, exported error value — a sentinel:
package store
import "errors"
var ErrNotFound = errors.New("not found")
func Lookup(key string) (string, error) {
v, ok := table[key]
if !ok {
return "", fmt.Errorf("key %q: %w", key, ErrNotFound)
}
return v, nil
}
The name starts with Err by convention. Because it's a single value, callers
can test for it — and note that Lookup wraps the sentinel rather than
returning it bare, so the error carries the key and remains matchable.
errors.Is: matching through the chain
You might be tempted to write err == store.ErrNotFound — but that fails the
moment anyone wraps the sentinel. errors.Is unwraps the chain link by link,
so it matches at any depth:
v, err := store.Lookup("missing")
if errors.Is(err, store.ErrNotFound) {
// handle the not-found case: use a default, return 404, ...
}
Rule: compare errors with errors.Is, never == (the lone exception is
err != nil itself).
errors.As: extracting a type
Sentinels identify a failure; sometimes you need data from it. When an
error is a custom struct type carrying fields, errors.As walks the chain
looking for that type and copies it out — a type assertion that understands
wrapping:
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
fmt.Println("failed path:", pathErr.Path)
}
Wrap deliberately
%w makes the wrapped error part of your API — callers may start relying on
errors.Is finding it, forever. If you want to add context without
promising the cause, use %v instead: same message, no chain. The habit to
build: wrap with context at every layer boundary, mention what you were
attempting, and don't repeat words the inner error already says.
Try it
Implement findUser, which wraps a sentinel ErrNotFound with the missing
user's name — the tests check both faces: errors.Is matches, and the
message contains the name.
Practice
1. Wrapping a Sentinel
IntermediateThe starter declares a sentinel error and a lookup function over a map of user names to IDs:
var ErrNotFound = errors.New("not found") func findUser(users map[string]int, name string) (int, error)Implement
findUser:- If
nameis a key inusers, return its ID and anilerror. - If it isn't, return
0and an error that wrapsErrNotFoundand includes the missing name, in the form:
fmt.Errorf("user %q: %w", name, ErrNotFound)so
findUser(users, "zed")fails with the messageuser "zed": not found.The wrapping matters: callers must be able to detect the case with
errors.Is(err, ErrNotFound)even though your error adds context on top. That only works if you use the%wverb —%vwould produce the same message but break the chain. The tests check both properties:errors.Ismatches, and the message contains the name.package main import ( "errors" "fmt" ) // ErrNotFound is the sentinel returned (wrapped) when a user doesn't exist. var ErrNotFound = errors.New("not found") // findUser returns the ID for name. When name is missing it returns 0 and // an error wrapping ErrNotFound that includes the name: user "zed": not found. func findUser(users map[string]int, name string) (int, error) { // TODO: implement return 0, nil } func main() { users := map[string]int{"ana": 1, "bo": 2} id, err := findUser(users, "ana") fmt.Println(id, err) _, err = findUser(users, "zed") if errors.Is(err, ErrNotFound) { fmt.Println("lookup failed:", err) } }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- If