Defer, Panic, and Recover Intermediate¶
Introduction¶
defer, panic, and recover are Go's mechanism for cleanup actions and handling unrecoverable errors. defer schedules a function call to run when the enclosing function returns — essential for closing files, unlocking mutexes, and releasing resources. panic halts normal execution and begins stack unwinding. recover catches a panic in a deferred function, letting you convert a crash into a returned error.
This trio is one of Go's most frequently asked interview topics because it tests understanding of stack unwinding, argument evaluation timing, and the boundary between errors and panics.
Syntax & Usage¶
Defer Basics¶
A deferred call executes when the surrounding function returns, regardless of how it returns (normal return, error return, or panic).
func readFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close() // runs when readFile returns
return io.ReadAll(f)
}
LIFO Order — Last In, First Out¶
Multiple defers execute in reverse order (stack behavior):
func main() {
defer fmt.Println("first")
defer fmt.Println("second")
defer fmt.Println("third")
}
// Output:
// third
// second
// first
Argument Evaluation at Defer Time¶
Defer arguments are evaluated when the defer statement executes, not when the deferred function runs:
func main() {
x := 10
defer fmt.Println("deferred x:", x) // x is captured as 10 NOW
x = 20
fmt.Println("current x:", x)
}
// Output:
// current x: 20
// deferred x: 10
To capture the final value, use a closure:
func main() {
x := 10
defer func() {
fmt.Println("deferred x:", x) // closure reads x at execution time
}()
x = 20
}
// Output:
// deferred x: 20
Defer with Named Return Values¶
Deferred functions can read and modify named return values:
func doWork() (result string, err error) {
defer func() {
if err != nil {
result = "" // clean up result on error
err = fmt.Errorf("doWork: %w", err)
}
}()
result = "data"
err = riskyOperation()
return
}
Common Defer Patterns¶
// File cleanup
f, err := os.Open(path)
if err != nil { return err }
defer f.Close()
// Mutex unlock
mu.Lock()
defer mu.Unlock()
// HTTP response body
resp, err := http.Get(url)
if err != nil { return err }
defer resp.Body.Close()
// Database transaction rollback
tx, err := db.Begin()
if err != nil { return err }
defer tx.Rollback() // no-op if tx.Commit() is called first
// Timing a function
func timed(name string) func() {
start := time.Now()
return func() {
fmt.Printf("%s took %v\n", name, time.Since(start))
}
}
defer timed("processData")()
Panic — When Things Go Wrong¶
Panic halts normal execution, runs deferred functions in the current goroutine, then crashes the program.
Runtime panics (triggered by Go itself):
var p *int
*p = 42 // nil pointer dereference
s := []int{1, 2, 3}
_ = s[10] // index out of range
ch := make(chan int)
close(ch)
ch <- 1 // send on closed channel
var m map[string]int
m["key"] = 1 // assignment to nil map
Explicit panic (triggered by programmer):
func MustParseConfig(path string) *Config {
cfg, err := ParseConfig(path)
if err != nil {
panic(fmt.Sprintf("failed to parse config %s: %v", path, err))
}
return cfg
}
When is panic acceptable?
- During initialization when the program cannot function without a resource (e.g., config, template).
- For truly impossible states — a switch default that should never be reached, invariant violations.
- In
Must*wrapper functions that convert errors to panics (convention: prefix withMust). - Never for expected runtime conditions like invalid user input, network failures, or missing files.
Recover — Catching Panics¶
recover() only works inside a deferred function. It stops the panic, returns the panic value, and lets the goroutine continue.
func safeDiv(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
return a / b, nil // panics if b == 0
}
result, err := safeDiv(10, 0)
fmt.Println(result, err) // 0 panic recovered: runtime error: integer divide by zero
recover() only works in deferred functions
Real-World: HTTP Recovery Middleware¶
The most common use of recover in production — prevent one panicking handler from crashing the entire server:
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
// Log the panic with stack trace
stack := debug.Stack()
log.Printf("PANIC: %v\n%s", rec, stack)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// Usage
mux := http.NewServeMux()
mux.HandleFunc("/", handler)
http.ListenAndServe(":8080", recoveryMiddleware(mux))
Panic/Recover vs Error Returns¶
// WRONG — using panic for expected errors
func FindUser(id int) *User {
u, err := db.Query(id)
if err != nil {
panic(err) // don't do this!
}
return u
}
// RIGHT — return errors for expected failures
func FindUser(id int) (*User, error) {
u, err := db.Query(id)
if err != nil {
return nil, fmt.Errorf("finding user %d: %w", id, err)
}
return u, nil
}
Quick Reference¶
| Concept | Behavior |
|---|---|
defer f() |
Runs f() when enclosing function returns |
| Multiple defers | Execute in LIFO (stack) order |
| Defer arguments | Evaluated at the defer statement, not at execution |
| Defer + closure | Closure captures variables by reference (reads final value) |
| Defer + named returns | Can read and modify named return values |
panic(v) |
Stops normal flow, runs defers, crashes if not recovered |
recover() |
Returns panic value in deferred function; nil otherwise |
| Runtime panics | Nil deref, index OOB, send on closed channel, nil map write |
Best Practices¶
- Use
deferfor every resource cleanup — files, mutexes, connections, response bodies. It's idiomatic and panic-safe. - Place
deferimmediately after acquiring the resource — makes the acquire/release pair visually obvious. - Prefer error returns over panic — panic is for programmer bugs and unrecoverable states, not for runtime conditions.
- Use
Must*functions only at init time —template.Must(),regexp.MustCompile(), or your ownMust*wrappers. - Always add recovery middleware in HTTP servers — one panicking handler should not take down the entire server.
- Log the stack trace on recovery — use
runtime/debug.Stack()to capture context for debugging. - Don't recover and silently ignore — at minimum, log the panic. Silent recovery hides bugs.
Common Pitfalls¶
Defer in a loop — resource leak
// BUG: all files stay open until the function returns
func processFiles(paths []string) error {
for _, path := range paths {
f, err := os.Open(path)
if err != nil { return err }
defer f.Close() // won't close until outer function returns!
process(f)
}
return nil
}
// FIX: extract to a helper function
func processFiles(paths []string) error {
for _, path := range paths {
if err := processOne(path); err != nil {
return err
}
}
return nil
}
func processOne(path string) error {
f, err := os.Open(path)
if err != nil { return err }
defer f.Close() // closes when processOne returns — each iteration
return process(f)
}
Defer argument evaluation surprise
func logDuration() {
start := time.Now()
defer fmt.Printf("took %v\n", time.Since(start)) // evaluated NOW — always ~0s
doWork()
}
// FIX: use a closure
func logDuration() {
start := time.Now()
defer func() {
fmt.Printf("took %v\n", time.Since(start)) // evaluated when deferred func runs
}()
doWork()
}
Recovering in the wrong place
Each goroutine must have its own recovery mechanism. A panic in a goroutine without recovery crashes the entire program.Ignoring the error from Close() in defer
// For write operations, Close() can return an error (e.g., flush failure)
func writeData(path string, data []byte) error {
f, err := os.Create(path)
if err != nil { return err }
defer f.Close() // if Close() fails, the error is silently lost
_, err = f.Write(data)
return err
}
// FIX: check Close error for writers
func writeData(path string, data []byte) (retErr error) {
f, err := os.Create(path)
if err != nil { return err }
defer func() {
if cerr := f.Close(); retErr == nil {
retErr = cerr
}
}()
_, err = f.Write(data)
return err
}
Performance Considerations¶
- Defer overhead: Each
deferallocates a small record on the heap (pre-Go 1.14) or stack (Go 1.14+, in most cases). In tight loops with millions of iterations, defer inside the loop adds measurable overhead — extract to a helper function. - Panic/recover cost: A recovered panic is expensive — it unwinds the stack, runs deferred functions, and captures state. Never use panic/recover as flow control. It's an order of magnitude slower than returning an error.
- Stack traces:
debug.Stack()andruntime.Stack()allocate. In recovery middleware, this is fine (panics are rare). Don't call them on hot paths. - Open-coded defers (Go 1.14+): Simple defers (no loops, no conditionals) are optimized to near-zero overhead by the compiler. Most real-world defers benefit from this.
Interview Tips¶
Interview Tip
"In what order do deferred calls execute?" LIFO — last in, first out. If you defer A, then B, then C, they execute C → B → A. This is natural for resource cleanup: you close the last-opened resource first.
Interview Tip
"When are defer arguments evaluated?" Arguments are evaluated at the defer statement, not when the deferred function runs. This is a classic gotcha: defer fmt.Println(x) captures the current value of x. To capture the final value, use a closure: defer func() { fmt.Println(x) }().
Interview Tip
"When should you use panic vs returning an error?" Errors are for expected failures — file not found, invalid input, network timeout. Panic is for programmer bugs — nil pointer where one is impossible, violated invariants, failed initialization of essential resources. In production code, panics should be rare and always caught by recovery middleware.
Interview Tip
"How does recover work?" recover() only works inside a deferred function. When called during a panic, it stops the panic, returns the panic value, and lets the goroutine continue. When called outside a panic (or not in a deferred function), it returns nil. Each goroutine must recover its own panics — you cannot recover a panic from a different goroutine.
Interview Tip
"What happens if a goroutine panics and doesn't recover?" The entire program crashes. Go does not have a global panic handler. This is why every goroutine you spawn should either be guaranteed not to panic, or should contain its own defer/recover. HTTP servers handle this at the handler level with recovery middleware.
Key Takeaways¶
deferruns when the enclosing function returns — use it for every resource cleanup.- Defers execute in LIFO order; arguments are evaluated at the
deferstatement. - Don't defer in loops — extract to a helper function to avoid resource leaks.
panicis for unrecoverable programmer errors, not for expected runtime failures.recover()only works inside a deferred function and only catches panics in the same goroutine.- Every HTTP server needs recovery middleware to prevent one bad handler from crashing the process.
- Named return values + defer closures enable powerful cleanup and error-wrapping patterns.