Frontend Labs
⋯

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)
	}
}

Generated from real-life-golang-guide.md · 8 Oct 2026 16:36