10. Error handling: the pattern that shapes all Go code
10.1 error is just an interface
type error interface {
Error() string
}
That's the entire contract. errors.New("message") and
fmt.Errorf("...") both return values satisfying it.
10.2 Wrapping with %w, and unwrapping with errors.Is/errors.As
var ErrNotFound = errors.New("not found")
func lookup(key string) (string, error) {
if _, ok := store[key]; !ok {
return "", fmt.Errorf("lookup %q: %w", key, ErrNotFound)
}
return store[key], nil
}
_, err := lookup("missing")
if errors.Is(err, ErrNotFound) {
// true even though the error message has extra context wrapped around it
}
errors.Is walks the Unwrap() chain checking for a target sentinel;
errors.As walks the chain looking for a target type so you can pull
out a custom error's fields:
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string { return e.Field + ": " + e.Msg }
var verr *ValidationError
if errors.As(err, &verr) {
fmt.Println("bad field:", verr.Field)
}
10.3 Sentinel errors vs. custom error types
Use a sentinel (var ErrX = errors.New(...)) when callers only need
to know which known condition occurred. Use a custom type when
callers need structured data about the failure (which field, what limit
was exceeded, etc). Both compose through wrapping.
10.4 What Go deliberately does not do: exceptions for control flow
There is no try/catch. panic/recover exist (Chapter 30) but are
reserved for programmer errors and unrecoverable states (index out of
range, nil dereference) — not for "user typed an invalid email." That
case is an error return, checked immediately at the call site. This is
why Go code has so many if err != nil { return err } lines: it's the
visible cost of making every failure path an explicit, reviewable branch
instead of an invisible jump.
10.5 Real-world example
Day 7's multi-layer error system (ErrNotFound, ErrStorageFull,
ErrInvalidKey, each wrapping context) is precisely the pattern a
production HTTP handler uses to map internal errors to status codes:
func handler(w http.ResponseWriter, r *http.Request) {
err := doOperation(r)
switch {
case errors.Is(err, ErrNotFound):
http.Error(w, err.Error(), http.StatusNotFound)
case errors.Is(err, ErrInvalidKey):
http.Error(w, err.Error(), http.StatusBadRequest)
case err != nil:
http.Error(w, "internal error", http.StatusInternalServerError)
default:
w.WriteHeader(http.StatusOK)
}
}