defer, panic & recover
defer schedules cleanup at function exit, panic starts stack unwinding, and recover can halt that unwind - but only inside a deferred call.
These three mechanisms are Go's answer to structured teardown without exceptions for normal errors.
Summary
defer pushes calls onto a per-goroutine LIFO stack executed when the surrounding function returns.
Arguments to deferred functions are evaluated immediately, but the call waits until return.
panic runs deferred calls while searching for a recover.
recover returns the panic value only during that unwind.
Expected failures should use error returns, not panic.
Recipe
Quick-reference recipe card - copy-paste ready.
package main
import (
"fmt"
"os"
)
func readConfig(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
buf := make([]byte, 64)
n, err := f.Read(buf)
if err != nil {
return "", err
}
return string(buf[:n]), nil
}
func main() {
text, err := readConfig("config.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(text)
}When to reach for this:
- Use
deferimmediately after a successful resource acquisition (Open,Lock,BeginTx). - Use
panicfor impossible states that indicate programmer bugs, not I/O failures. - Use
recoverat process boundaries (HTTP middleware, worker top-level) to log and return 500. - Return
errorfor anything a caller could reasonably handle or retry.
Working Example
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
fmt.Printf("worker %d recovered: %v\n", id, r)
}
}()
if id == 2 {
panic("simulated bug")
}
fmt.Println("worker", id, "ok")
}
func main() {
var wg sync.WaitGroup
for id := 1; id <= 3; id++ {
wg.Add(1)
go worker(id, &wg)
}
wg.Wait()
fmt.Println("all workers finished")
}What this demonstrates:
defer wg.Done()runs even whenpanicfires later in the function.- Inner
deferwithrecovercatches panic without killing the whole program. - Other goroutines continue - panic is per-goroutine unless it reaches
main. - Ordering: deferred calls run LIFO before the function truly returns.
Deep Dive
How It Works
- Each goroutine maintains a defer stack; pushing happens at
deferstatement execution time. - Deferred function arguments are evaluated at the
deferline, not at unwind time (watch loop variables and pointers). - On
panic, the runtime runs deferred functions from innermost outward untilrecoverstops propagation or the stack ends. - If panic is not recovered, the program prints a stack trace and exits (or kills the goroutine in workers).
recover()outside a deferred function always returnsnil.
defer / panic / recover Timeline
| Phase | What happens |
|---|---|
defer f() | Push f onto defer stack; evaluate args now |
| Normal return | Run all defers LIFO, then return to caller |
panic(v) | Begin unwind; run defers LIFO on this goroutine |
recover() in defer | Stop unwind; return v to deferred func |
| Unrecovered panic | Crash goroutine / process with stack trace |
Go Notes
// Named result + defer can adjust return values
func divide(a, b int) (q int, err error) {
defer func() {
if b == 0 {
err = fmt.Errorf("divide by zero")
}
}()
return a / b, nil // careful: division still panics on zero in this naive form
}
// Prefer explicit check before division instead of relying on panicGotchas
- Defer in loops -
for { defer f() }grows the defer stack until the function returns. Fix: wrap loop body in a nested function or defer outside the hot loop. - Capturing loop variables in defer args - Arguments evaluate once per
deferstatement; closures in deferred func literals see final values. Fix: pass values as parameters:defer func(v int) { ... }(i). - Recover outside defer -
recover()returns nil and does not stop panic. Fix: only callrecoverinsidedefer func() { ... }(). - Swallowing all panics silently - Empty recover hides bugs. Fix: log stack, increment metrics, return 500 to clients.
- Using panic for control flow - Makes APIs hard to use and test. Fix: return
erroror a(T, bool)result. - Defer cost in tight loops - Measurable overhead in benchmarks. Fix: hoist defer to outer function or use explicit cleanup when proven hot.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
error return | Expected failures | Truly unrecoverable invariant breaks |
sync.Once cleanup | One-time teardown | Per-request resources |
context.Context cancel | Propagate shutdown | Simple Close() on one handle |
try/finally pattern via defer | Always need cleanup | Language lacks finally - defer is the idiom |
| Process supervisor | Isolate crashing workers | In-process HTTP handlers (use recover) |
FAQs
When are defer arguments evaluated?
At the defer statement, not when the deferred call runs.
Loop variables in arguments need careful copying.
What order do multiple defers run?
Last deferred runs first (LIFO).
Mirror resource acquisition order in reverse for release.
Does defer run on panic?
Yes - that is a primary use case for cleanup.
Deferred functions run while the stack unwinds.
Can recover restart execution after panic?
Recover stops unwind in the current goroutine.
Execution continues after the deferred function that recovered, not at the panic site.
Should libraries panic?
Libraries should return errors for operational failures.
Panic only for misuse that callers cannot prevent.
What happens if panic happens in a defer?
A new panic during unwind can override the previous panic value.
Keep deferred functions simple and panic-free.
Is defer slow?
Small fixed cost per defer.
Avoid thousands of defers in one function - refactor loops.
Can I defer a method call?
Yes - defer f.Close() and defer mu.Unlock() are standard.
Receiver is evaluated with other arguments at defer time.
Does recover catch panics from other goroutines?
No - each goroutine unwinds independently.
Middleware recover only protects the handler goroutine it wraps.
How do I log the stack on recover?
Use debug.Stack() or runtime.Stack inside the deferred recover block.
Include request IDs from context for HTTP services.
Related
- Control Flow in HTTP Middleware - recover at request boundaries
- Control Flow Basics - first defer and recover snippets
- Go Control Flow: Expression-Oriented Design - how defer fits expression-oriented flow
- if with Initialization & switch Patterns - open files then defer close
- Control Flow Best Practices - when not to panic
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).