Lesson 10 · Errors & idiomatic Go
Errors are values
No exceptions. You return an error, add context, and check it.
Your win: explain Go's "errors as values" model, wrap an error with
%w to add context without losing the original, define a sentinel — and spot
the %v bug hiding in our own handlers.
An error is just a value implementing one method
error is an ordinary interface: interface { Error() string }.
There are no exceptions in Go — a function that can fail returns an error
as its last result, and the caller checks it. Control flow stays visible.1
resp, err := svc.CreateEmail(ctx, payload)
if err != nil {
return nil, err // explicit — no hidden unwind
}
Create errors with errors.New("msg") or fmt.Errorf(...).
A sentinel error is a package-level value you can compare
against later:
var (
ErrEmpty = errors.New("empty")
ErrMediasSizeExceed = errors.New("medias size exceed")
ErrInternal = errors.New("internal")
)
Wrapping: add context, keep the original
As an error bubbles up, each layer adds context with fmt.Errorf and the
%w verb. %w wraps — it embeds the original error so it
can be recovered later (Lesson 11), while prepending a breadcrumb.2
return fmt.Errorf("multierr.Combine: %w", err)
return fmt.Errorf("repo.queueUpsert: %w", err)
// A reader sees: "repo.queueUpsert: multierr.Combine: <root cause>"
%v instead of %w
(create_email_handler.go:34,45). %v formats
the error to text and drops the reference — so the returned
error does not wrap the original. Any downstream errors.Is /
errors.As (Lesson 11) is then blind to the root cause. It's a subtle,
common mistake — and a perfect interview "what's wrong with this line?"
go.uber.org/multierr
(multierr.Combine/Append, repositories/email.go)
and some layers use github.com/pkg/errors's errors.Wrap. The
rule of thumb documented in the repo: wrap at each call site with
fmt.Errorf("call.Site: %w", err) so the final message reads like a stack.
Why Go chose this
Errors-as-values makes failure a normal, visible part of the signature — you can't
forget an error is possible, and there's no invisible throw three frames
down. The cost is verbosity (if err != nil everywhere); the benefit is that
reading a function tells you exactly how it can fail. Interviewers want the trade, not
just the complaint.
The Go Blog — "Working with Errors in Go 1.13"
The canonical explanation of %w wrapping (and Is/As,
which we cover next). Pair with Effective Go's errors section.
Check yourself (from memory)
Q1. In Go, an error is…
error is a plain interface value returned
and checked — no exceptions, explicit control flow.
Q2. fmt.Errorf("load: %w", err) differs from %v because it…
%w keeps the original reachable for
errors.Is/As; %v flattens it to a string and breaks the chain.
Q3. A sentinel error is…
var ErrX = errors.New(...) — a named value
you compare against with errors.Is. Our validator has several.
fmt.Errorf("x: %v", err) quietly break downstream error handling?%v renders the error to text and drops the reference,
so the returned error does not wrap the original. Downstream
errors.Is(err, ErrTarget) / errors.As can't unwrap to find
it — the chain is severed. Use %w to keep the original reachable. (This is
a real bug in create_email_handler.go.)