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
- Q507What does indexing and ranging over a UTF-8 string print?
- Q508This compiles. Why does it panic at runtime?
- Q510Go 1.23 range-over-func: what does this buggy iterator do?
- Q511What does this counter print, and how do you prove the bug?
- Q512Deleting from a slice in place: what do both lines print?
- Q513Embedding is not inheritance: what does this print, and which line does not compile?