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-281switch {
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: ...
}
}
errorx.ToStatusError
(internal/golibs/errorx/errorx.go:56-78) uses this
pattern to turn a Postgres driver error into a gRPC status:
UniqueViolation → codes.AlreadyExists,
ForeignKeyViolation → codes.InvalidArgument. That only works
because the error was wrapped with %w the whole way up.
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.
%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.
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.
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…
%w keeps the original reachable for
unwrapping. %v/%s flatten it to text and sever the chain.
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.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.