Three tools, three different questions.
errors.Is(err, target) — “is this this specific error?”
Compares against a sentinel value. Walks the wrap chain (so fmt.Errorf("%w: bad scheme", ErrInvalidURL) still matches).
if errors.Is(err, store.ErrInvalidURL) { ... }
if errors.Is(err, io.EOF) { ... }
if errors.Is(err, sql.ErrNoRows) { ... }
Use when you have a named error value and just want to know “is the failure of this kind?”
Why not ==? == only checks the outermost error. The moment someone wraps it (fmt.Errorf("%w: ...", err)), == breaks. errors.Is keeps working.
errors.As(err, &target) — “is this a kind of error, and give me the value”
Walks the wrap chain looking for an error of a specific type, and assigns it into your variable so you can read its fields.
var pathErr *os.PathError
if errors.As(err, &pathErr) {
log.Println(pathErr.Path, pathErr.Op)
}
var urlErr *url.Error
if errors.As(err, &urlErr) {
log.Println(urlErr.URL)
}
Use when the error carries data you care about (paths, URLs, fields, codes), not just an identity.
errors.AsType[T](err) (T, bool) — same as As, but generic
Added in Go 1.25. Identical semantics to As, just nicer ergonomics — returns the value instead of writing through a pointer.
if pathErr, ok := errors.AsType[*os.PathError](err); ok {
log.Println(pathErr.Path)
}
Use it instead of As if you’re on 1.25+. Functionally equivalent; pick the one you find more readable. The Go community will probably trend toward AsType over time.
Mental model
| Question you’re asking | Tool |
|---|---|
”Is the failure mode X?” (sentinel) | errors.Is |
”Is the error structurally of type T and what’s inside it?” | errors.As / errors.AsType |
Sentinels are simpler — prefer them when an identity check is enough. Reach for As/AsType only when you actually need the fields.
In a typical store package with sentinel errors like ErrInvalidURL, ErrDuplicate, ErrNotFound, errors.Is is the right call — they’re pure sentinels with no payload. The day you wrap them with detail like fmt.Errorf("%w: scheme %q", ErrInvalidURL, parsed.Scheme), errors.Is still works — that’s the whole point of using sentinels + wrapping.