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.
Search across all documentation pages
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.
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.
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:
defer immediately after a successful resource acquisition (Open, Lock, BeginTx).panic for impossible states that indicate programmer bugs, not I/O failures.recover at process boundaries (HTTP middleware, worker top-level) to log and return 500.error for anything a caller could reasonably handle or retry.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 when panic fires later in the function.defer with recover catches panic without killing the whole program.main.defer statement execution time.defer line, not at unwind time (watch loop variables and pointers).panic, the runtime runs deferred functions from innermost outward until recover stops propagation or the stack ends.recover() outside a deferred function always returns nil.| 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 |
// 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 panicfor { defer f() } grows the defer stack until the function returns. Fix: wrap loop body in a nested function or defer outside the hot loop.defer statement; closures in deferred func literals see final values. Fix: pass values as parameters: defer func(v int) { ... }(i).recover() returns nil and does not stop panic. Fix: only call recover inside defer func() { ... }().error or a (T, bool) result.| 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) |
At the defer statement, not when the deferred call runs.
Loop variables in arguments need careful copying.
Last deferred runs first (LIFO).
Mirror resource acquisition order in reverse for release.
Yes - that is a primary use case for cleanup.
Deferred functions run while the stack unwinds.
Recover stops unwind in the current goroutine.
Execution continues after the deferred function that recovered, not at the panic site.
Libraries should return errors for operational failures.
Panic only for misuse that callers cannot prevent.
A new panic during unwind can override the previous panic value.
Keep deferred functions simple and panic-free.
Small fixed cost per defer.
Avoid thousands of defers in one function - refactor loops.
Yes - defer f.Close() and defer mu.Unlock() are standard.
Receiver is evaluated with other arguments at defer time.
No - each goroutine unwinds independently.
Middleware recover only protects the handler goroutine it wraps.
Use debug.Stack() or runtime.Stack inside the deferred recover block.
Include request IDs from context for HTTP services.
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).
Reviewed by Chris St. John·Last updated Jul 18, 2026