← All posts

2026-08-12

errors.Is vs errors.As: A Field Guide

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.

The rule of thumb

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.