errors.Is and errors.As both walk an error's Unwrap chain looking for
something. That's where the similarity ends, and it's also exactly why
they're easy to reach for backwards.
errors.Is: "is this a sentinel I recognize?"Use it when you're comparing against a known, specific error value —
usually a package-level var like sql.ErrNoRows or io.EOF.
row := db.QueryRow(query, id)
var name string
if err := row.Scan(&name); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil // not found isn't an error here
}
return nil, err
}
errors.Is unwraps the chain and compares each error against the target
with ==, unless an error in the chain implements Is(error) bool itself
(useful when equality isn't literal identity — comparing by code, not by
pointer). Reaching for == directly instead of errors.Is is the usual
mistake: it breaks the moment the error gets wrapped with fmt.Errorf("...: %w", err) anywhere between where it's returned and where it's checked.
errors.As: "is there an error of this type in here, and can I have it?"Use it when you want to pull a concrete error type out of the chain to read its fields — not just confirm it's there.
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
log.Printf("failed on %s: %v", pathErr.Path, pathErr.Err)
}
errors.As walks the chain looking for an error assignable to the type of
*target, and if it finds one, assigns it — that's why the second argument
is a pointer. This is the one people reach for errors.Is to do and then
get confused when it won't compile: errors.Is compares against a specific
value, and most custom error types aren't comparable sentinels, they're
structs you want the fields out of.
If you're checking "is this the well-known error" — errors.Is. If you're
checking "is this a particular kind of error, and I need what's inside it"
— errors.As. Both exist because plain == and type assertions stopped
being enough the moment wrapping errors with context became idiomatic — and
both are worth using instead of writing your own unwrap loop, because the
standard library's version already handles multi-error trees (from
errors.Join) correctly, and a hand-rolled one usually doesn't.