How does errors.Is work internally, and how can a custom type change its matching?
Question 297MediumGo 1.22 to 1.25
errors.Is(err, target) walks the error tree depth-first. At each node it returns true if the node == target (only when target's type is comparable) or if the node has an Is(error) bool method that returns true. It follows Unwrap() error and Unwrap() []error. errors.Is(nil, nil) is true, and errors.Is(err, nil) is true only when err == nil.
type HTTPError struct{ Code int }
func (e *HTTPError) Error() string { return "http " + strconv.Itoa(e.Code) }
// Match any *HTTPError with the same code, not just the same pointer.
func (e *HTTPError) Is(target error) bool {
t, ok := target.(*HTTPError)
return ok && t.Code == e.Code
}
err := fmt.Errorf("call api: %w", &HTTPError{Code: 404})
fmt.Println(errors.Is(err, &HTTPError{Code: 404})) // true
fmt.Println(errors.Is(err, &HTTPError{Code: 500})) // false
A custom Is method should only do a shallow comparison. It must not call Unwrap or errors.Is itself, because the library already walks the chain. Is answers "is this equal to that", while As answers "is this of that type".
More on Error Handling & panics
- Q295What does this print, and why?
errors.New("x") == errors.New("x") - Q296What is the difference between
%wand%vinfmt.Errorf? - Q298Explain
errors.As. Why must the target be a pointer, and what happens if you get it wrong? - Q299What is
errors.Join? How do multi-errors interact witherrors.Is,errors.Asanderrors.Unwrap? - Q300Compare sentinel errors, typed errors and opaque errors. When would you use each?
- Q301How do you design a good custom error type? Does it matter whether
Error()has a pointer or value receiver?