Go's defer, panic, and recover form a trio for resource management and exceptional failure handling. defer schedules a function call to execute when the surrounding function returns — regardless of whether it returns normally, via an early return, or through a panic. Deferred calls execute in LIFO (last-in, first-out) order, and their arguments are evaluated at the point of the defer statement, not at the point of execution.
panic halts normal execution of the current goroutine, runs all deferred functions in the current stack frame, then continues unwinding up the call stack. recover is the only way to stop a panic from crashing the program — it must be called inside a deferred function, where it captures the panic value and resumes normal execution. Unlike exceptions in other languages, panics are not for routine error handling. They signal programmer errors (index out of bounds, nil pointer dereference) or truly unrecoverable situations.
The practical split is clear: use defer constantly for cleanup (closing files, releasing locks, stopping timers), return errors for expected failures, and reserve panic/recover for protecting goroutine boundaries in servers or for package-internal invariant violations that get converted to errors at the API surface.
Master go defer, panic, and recover
Take the Go Programming course with hands-on lessons and challenges.
Place defer immediately after acquiring a resource. This keeps open and close visually together and guarantees cleanup on every exit path — normal return, early error return, or panic. The pattern applies to files, mutexes, database transactions, HTTP response bodies, and timers.
LIFO order means defer works like a stack — useful for paired operations (lock/unlock, open/close). Arguments are evaluated eagerly: defer fmt.Println(x) captures x's current value. If you need the value at execution time, wrap the call in a closure that captures the variable by reference.
Named return values let deferred functions modify the return value. This is the standard pattern for database transaction helpers: the deferred function inspects err (set by fn) to decide whether to commit or rollback, and it handles panics by rolling back and converting the panic to an error.
This is the primary legitimate use of recover in production: catching panics at goroutine boundaries. Without recovery middleware, a single nil pointer dereference in one handler would crash the entire server. The deferred recover logs the stack trace for debugging while keeping the server running.
Some packages (like encoding/json and text/template in the stdlib) use panic internally to unwind deeply nested recursive calls, then convert those panics to errors at the public function boundary. The key safety rule: only recover your own panic types and re-panic anything else.
The track/duration pair exploits defer's eager evaluation: track() runs immediately (capturing the start time), and duration runs at function exit (computing elapsed time). This is a clean, reusable pattern for measuring function execution time in logs or metrics.
Deferring a method call inside a loop without realizing every iteration adds a deferred call that won't execute until the function returns
Deferred calls are tied to the enclosing function, not the loop. If you're opening files in a loop, extract the loop body into a separate function so each iteration's defer executes at the end of that function. Otherwise, you'll hold all resources open until the outer function returns.
Using panic for routine error handling instead of returning errors
Panic is for unrecoverable situations — programmer bugs, violated invariants, or impossible states. Expected failures (file not found, invalid input, network timeout) should always be returned as errors. Callers should never need recover() to use your package safely.
Calling recover() outside of a deferred function, where it always returns nil
recover() only captures a panic value when called directly inside a deferred function. Calling it in normal code, or in a helper function called from a deferred function, returns nil. The pattern is always: defer func() { if r := recover(); r != nil { ... } }()
Ignoring the error returned by the deferred Close() call, potentially losing data on writes
For read-only resources, defer f.Close() is fine. For writable resources (files, response bodies being written), capture the close error with named returns: defer func() { if cerr := f.Close(); err == nil { err = cerr } }(). A failed Close on a write can mean data wasn't flushed to disk.
defer guarantees cleanup by scheduling function calls to run when the enclosing function returns, in LIFO order, with eagerly evaluated arguments. panic halts normal execution and unwinds the stack, running deferred functions at each level. recover, called inside a deferred function, captures the panic value and resumes normal execution. Use defer constantly for resource cleanup, return errors for expected failures, and reserve panic/recover for protecting goroutine boundaries and converting internal invariant violations to errors at package APIs.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.