When is err == ErrX wrong, and why can comparing errors with == panic?
Question 303HardGo 1.22 to 1.25
== matches only the outermost error. As soon as someone wraps it with %w, the comparison fails. Use errors.Is, which walks the chain. The exceptions are documented unwrapped sentinels such as io.EOF returned by Read, where == io.EOF is idiomatic but errors.Is is still safe.
Comparing two interfaces whose dynamic type is not comparable (slice, map or func-based types) panics at run time. errors.Is guards against this: it only uses == if the target's type is comparable.
type MultiErr []error
func (m MultiErr) Error() string { return "multi" }
func main() {
var e1 error = MultiErr{io.EOF}
var e2 error = MultiErr{io.EOF}
fmt.Println(errors.Is(e1, e2)) // false, no panic
fmt.Println(e1 == e2) // panic: runtime error: comparing
// uncomparable type main.MultiErr
}
Takeaways: prefer errors.Is, and give error types that contain slices or maps a pointer receiver, so the dynamic type is a comparable pointer. Also, a switch err { case ErrA: } uses ==, so it has the same wrapping problem. Write switch { case errors.Is(err, ErrA): } instead.
More on Error Handling & panics
- Q301How do you design a good custom error type? Does it matter whether
Error()has a pointer or value receiver? - Q302The nil error interface gotcha: what does this print?
- Q304When should you wrap an error and when should you not? How do you handle errors at package boundaries?
- Q305When is it appropriate to
panicinstead of returning an error? - Q306What does this print? (Rules of
recover) - Q307A goroutine panics. Can a
recoverinmaincatch it? How do you protect background goroutines?