Go

Why does errors.Is return different results here?

Question 509MediumGo 1.22 to 1.25
var ErrNotFound = errors.New("not found")

e1 := fmt.Errorf("get user: %v", ErrNotFound)
e2 := fmt.Errorf("get user: %w", ErrNotFound)

fmt.Println(e1)                           // get user: not found
fmt.Println(e2)                           // get user: not found
fmt.Println(errors.Is(e1, ErrNotFound))   // false
fmt.Println(errors.Is(e2, ErrNotFound))   // true
fmt.Println(e2 == ErrNotFound)            // false

Both errors have the same text, but %v only formats the message into a string, and the link to the original error is gone. %w builds a *fmt.wrapError with an Unwrap() error method, and errors.Is and errors.As follow that chain.

== fails for e2 because it compares only the outer value. That is why code that uses sentinel errors must switch from err == ErrX to errors.Is once wrapping is introduced.

Senior-level points: since Go 1.20, fmt.Errorf accepts several %w verbs and errors.Join combines errors. Their Unwrap() []error is searched as a tree. Wrapping makes the inner error part of your API contract, so use %v on purpose when you do not want callers to depend on an implementation detail, such as a particular database driver's error.

More on Tricky Output & Code-Review Puzzles

All 38 Tricky Output & Code-Review Puzzles questions