Go

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

All 37 Error Handling & panics questions