Errors & Generics
Error handling in Go
Section titled “Error handling in Go”Most languages handle failures through exceptions — a special control-flow mechanism that unwinds the call stack until a catch block intercepts the problem. Go takes a radically different approach: errors are ordinary values, returned alongside a result and checked explicitly with if err != nil. There is no hidden control flow and no unchecked exceptions — every failure point is visible at the callsite. The next lesson, Error Values, covers the error interface and this pattern in full detail.
The second half of this module is a different kind of tool entirely: generics let one function or type work across many concrete types, checked at compile time instead of duplicated by hand or type-asserted at runtime. Panic and recover sit in between — an escape hatch for programmer errors and impossible states, never a substitute for returning (T, error).
One thing worth carrying forward from Functions before you reach Panic & Recover: deferred calls run in LIFO order — the last defer registered runs first. That rule does not change during a panic; it is exactly why recover() has to live inside a deferred function in the first place.
Every rule in this module resolves to one of a few concrete things at the keyboard: a nil check that fires when it shouldn’t, a compiler error naming a missing pointer or an unsatisfied constraint, or a panic message telling you precisely what went wrong. Learning to recognize the message, not just the rule in the abstract, is the actual skill this module builds.
Module map
Section titled “Module map”| Lesson | What you will learn |
|---|---|
| Error Values | The error interface, errors.New, sentinel errors, the typed-nil interface trap, errors.Is. |
| Wrapping Errors | %w, errors.As, errors.Join, custom error types, why receiver choice matters. |
| Panic and Recover | When to panic, recover, runtime.Error, panic(nil), why goroutines can’t be protected from outside. |
| Generics | Type parameters, constraints, comparable, cmp.Ordered, generic methods (Go 1.27). |
| Iterators | iter.Seq, range-over-func, iter.Pull, the slices/maps iterator helpers (Go 1.23+). |
A first look
Section titled “A first look”Here is a minimal function that returns (string, error), demonstrating the fundamental shape of Go error handling:
package main
import ( "errors" "fmt")
func greet(name string) (string, error) { if name == "" { return "", errors.New("name must not be empty") } return fmt.Sprintf("Hello, %s!", name), nil}
func main() { msg, err := greet("Gopher") if err != nil { fmt.Println("error:", err) return } fmt.Println(msg) // Hello, Gopher!
_, err = greet("") if err != nil { fmt.Println("error:", err) // error: name must not be empty }} errors.New creates a simple error value from a static string. The function returns nil as the error on the happy path and a non-nil error on the failure path. The caller checks if err != nil immediately after the call — this is the idiom you will write hundreds of times in Go.
Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
| explicit error handling | every failure point is visible in the code | more verbose than try/catch |
| error as value (not exception) | composable, predictable control flow | caller must check the error every time |
| generics (Go 1.18+) | type-safe reuse without interface{} | more compile time, more complex syntax |
| panic/recover | aborts immediately when an invariant is violated | not a general error-handling mechanism |
Common Misconceptions
Section titled “Common Misconceptions”- Go’s error handling is too verbose — the verbosity is intentional: every failure path is visible in the code, with no hidden exceptions.
- panic is an exception that should replace returning an error — panic is for programmer errors only, not expected failures.
- generics make code slower — Go generics use GC-shape stenciling: types that share the same pointer/interface shape share a single instantiation and pass type info through a dictionary at runtime, so generics can even be slower than an interface in that case; value types (
int, structs) get their own dedicated code per type and stay fast.
Reading AI-generated code
Section titled “Reading AI-generated code”Across every lesson in this module, the same handful of checks catch most generated-code bugs before they ship:
- Does a function return a typed pointer variable directly through an
error-typed result, instead of a barenilon the success path? That is the typed-nil interface trap, anderr == nilwill be wrong. - Is
errors.Ascalled with the target variable itself instead of its address (&target)? The first form panics; it never just returnsfalse. - Does a
recover()call sit inside a helper function the deferred function calls, rather than directly inside thedefer func() { ... }()body? One level of indirection is enough to make it always returnnil. - Is a constraint interface (one with
|or~terms) used as the type of an ordinary variable, instead of only inside[T Constraint]? - Does code call
sort.Strings, or similar, directly on the result ofmaps.Keys? Since Go 1.23 that returns aniter.Seq, not a slice, and won’t compile.
🐛 Spot the bug
type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }
func mightFail(fail bool) error { var e *MyError if fail { e = &MyError{msg: "boom"} } return e}
func main() { err := mightFail(false) fmt.Println(err == nil)}Show the answer
mightFail returns the *MyError-typed variable e directly, even when e is nil. The returned error interface then has a concrete type (*MyError) and a nil value — that is a non-nil interface holding a nil pointer, not a nil interface. Verified on 1.26.5, this prints false, not true. The fix is to hold *MyError only in a local variable and return a bare, untyped nil on the success path — see Error Values for the full explanation and a runnable playground.