defer, panic & recover

intermediate go errors defer panic

These three keywords are Go’s tools for cleanup and for handling the truly exceptional. They sound scary but they’re simple once we see them in action.

defer

defer schedules a function call to run when the surrounding function exits — no matter how it exits (normal return, or even a panic).

In simple language: “do this later, right before I leave, whatever happens.”

Why do we love it? Cleanup. We open something, immediately defer closing it, and never worry about forgetting — even if the function returns from ten different spots.

func readFile(name string) error {
	f, err := os.Open(name)
	if err != nil {
		return err
	}
	defer f.Close() // guaranteed to run when readFile exits
	// ... do a bunch of stuff with f, return wherever we want ...
	return nil // f.Close() fires right after this
}

defer runs in LIFO order

If we defer multiple things, they run last-in-first-out — like a stack of plates. The last one we deferred runs first.

func main() {
	defer fmt.Println("1") // runs last
	defer fmt.Println("2") // runs second
	defer fmt.Println("3") // runs first
	fmt.Println("start")
}
// Output:
// start
// 3
// 2
// 1
defer stack — pushed top to bottom, popped bottom to top
defer "3"  ← pushed last, runs FIRST
defer "2"
defer "1"  ← pushed first, runs LAST
↓ function exits → pop 3, then 2, then 1

The big gotcha: arguments are evaluated immediately

This trips up everyone. When we defer foo(x), Go evaluates x right now, at the defer line — not later when foo actually runs. The call is delayed, but the arguments are frozen at defer time.

func main() {
	i := 0
	defer fmt.Println("deferred:", i) // i is captured as 0 RIGHT HERE
	i = 99
	fmt.Println("now:", i)
}
// Output:
// now: 99
// deferred: 0   <-- not 99! it grabbed i when we deferred

If we actually want the latest value, wrap it in a closure (an inline function) so the read happens at exit time:

func main() {
	i := 0
	defer func() { fmt.Println("deferred:", i) }() // reads i when it runs
	i = 99
}
// Output:
// deferred: 99

The other classic use of defer is unlocking a mutex right after locking it, so we can never forget:

mu.Lock()
defer mu.Unlock() // unlock happens automatically on every exit path
// ... critical section ...

panic

panic stops the normal flow of a function. It unwinds the stack (running any deferred functions on the way up), and if nobody catches it, the whole program crashes and prints a stack trace.

Think of it like pulling the fire alarm. It’s loud, dramatic, and meant for emergencies — not everyday “file not found” stuff.

When should we use it? Only for truly unrecoverable situations — bugs that should never happen, broken invariants, impossible states. For normal failures, we return an error (see the errors note). Don’t use panic for control flow.

func mustGetEnv(key string) string {
	v := os.Getenv(key)
	if v == "" {
		// the app literally cannot run without this — fine to panic
		panic("missing required env var: " + key)
	}
	return v
}

recover

recover stops a panic mid-unwind and lets the program keep running. The catch: it only works inside a deferred function. Anywhere else, it does nothing.

In simple language: recover is the net we hang under the trapeze. It only counts if it’s set up (deferred) before the fall.

The classic real-world use is a server that shouldn’t die just because one request handler panicked:

func safeHandler(w http.ResponseWriter, r *http.Request) {
	defer func() {
		if rec := recover(); rec != nil { // catch the panic, if any
			log.Printf("recovered from panic: %v", rec)
			http.Error(w, "internal error", 500) // tell the client, stay alive
		}
	}()
	riskyWork(r) // if this panics, the deferred func above catches it
}
panic()
flow stops, stack starts unwinding
deferred funcs run
in LIFO order, on the way up
recover()
caught → program lives. not caught → crash

Interview soundbite

defer schedules a call to run when the function exits, in LIFO order, with its arguments evaluated immediately at the defer line — perfect for closing files and unlocking mutexes. panic unwinds the stack and is reserved for truly unrecoverable bugs, while recover — which only works inside a deferred function — catches a panic so something like a web server can log it and keep serving other requests instead of crashing.