Lesson 11 · Errors & idiomatic Go

errors.Is / errors.As

Inspecting a wrapped error chain — the payoff for using %w.

Your win: use errors.Is to test for a specific error and errors.As to extract a typed one, both through the wrap chain — and explain why they need %w (Lesson 10) to work.

Why == isn't enough anymore

Once errors are wrapped, err == ErrNotFound fails — err is now the outer wrapper, not the sentinel itself. errors.Is fixes that by walking the whole chain (unwrapping each %w) looking for a match.1

// Test for a specific error, however deeply wrapped:
if errors.Is(err, io.EOF) { ... }        // consumer.go:178
if errors.Is(err, ErrMediasSizeExceed) { ... }

Our code uses exactly this to turn a wrapped sentinel into the right response:

internal/spike/modules/email/controller/grpc/validator.go:278-281
switch {
case errors.Is(result.err, ErrInternal):        // → codes.Internal
case errors.Is(result.err, ErrMediasSizeExceed): // → codes.ResourceExhausted
}

errors.As: extract a typed error to read its fields

When you need more than "is it this error?" — you need the error's data — use errors.As. It walks the chain for an error of a given type and, if found, assigns it to your variable.1

var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
    switch pgErr.Code {              // now read the driver's fields
    case pgerrcode.UniqueViolation: ...
    }
}
Anchor — our PG→status translator errorx.ToStatusError (internal/golibs/errorx/errorx.go:56-78) uses this pattern to turn a Postgres driver error into a gRPC status: UniqueViolationcodes.AlreadyExists, ForeignKeyViolationcodes.InvalidArgument. That only works because the error was wrapped with %w the whole way up.
Is vs As, in one line errors.Is answers "is this the error?" (compare to a value). errors.As answers "is there an error of this type — give it to me" (extract to read fields). Both traverse the %w chain.
The connection to Lesson 10 Neither works across a link that used %v. Our create_email_handler.go %v wrap means a caller's errors.Is would miss a sentinel underneath it. Wrapping and inspection are two halves of one system — break one and the other fails silently.
Read this next

The Go Blog — "Working with Errors in Go 1.13"

The same article as Lesson 10 — reread the Is/As sections now that you've seen them in our code. The errors package docs are the exact reference.

go.dev/blog/go1.13-errors
pkg.go.dev/errors

Check yourself (from memory)

Q1. errors.Is(err, ErrNotFound) beats err == ErrNotFound because it…

== only matches the outermost value; errors.Is unwraps each %w to find the sentinel anywhere in the chain.

Q2. To extract a custom/driver error type and read its fields, use…

errors.As(err, &target) finds an error of that type in the chain and assigns it — how errorx.ToStatusError reads *pgconn.PgError.Code.

Q3. errors.Is can find a wrapped sentinel only if wrapping used…

Only %w keeps the original reachable for unwrapping. %v/%s flatten it to text and sever the chain.
errors.Is vs errors.As — when do you reach for each?
recall, then click to reveal
errors.Is(err, target) — does the chain contain this specific error value (a sentinel like io.EOF or ErrNotFound)? Returns a bool. errors.As(err, &target) — does the chain contain an error of this type, and if so hand it to me so I can read its fields (e.g. *pgconn.PgError)? Both walk the %w chain; neither sees through a %v link.
Want to build a custom error type with an Unwrap() method so it plays nicely with Is/As? Ask me.

1. The Go Blog — Working with Errors in Go 1.13; errors package docs.