Go

Go Defer, Panic, and Recover👨‍💻

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.

Key Takeaways

  • 1Deferred functions execute in LIFO order when the enclosing function returns — the last defer registered runs first
  • 2Deferred function arguments are evaluated immediately at the defer statement, not when the deferred function actually executes
  • 3defer runs even if the function panics, making it the only reliable way to guarantee cleanup in the presence of panics
  • 4panic stops normal execution, unwinds the goroutine stack running deferred functions at each level, and terminates the program if not recovered
  • 5recover only works when called directly inside a deferred function — calling it in a normal function or a nested function inside a deferred function returns nil
  • 6The idiomatic pattern is to return errors for expected failures and reserve panic for programming bugs or unrecoverable invariant violations

Master go defer, panic, and recover

Take the Go Programming course with hands-on lessons and challenges.

Examples

Defer for guaranteed resource cleanup

go

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 execution order and argument evaluation timing

go

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.

Defer with named return values for error enrichment

go

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.

Panic and recover — protecting a server from goroutine crashes

go

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.

Converting panics to errors at package boundaries

go

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.

Defer for timing and observability

go

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.

Common Mistakes

Mistake:

Deferring a method call inside a loop without realizing every iteration adds a deferred call that won't execute until the function returns

Fix:

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.

Mistake:

Using panic for routine error handling instead of returning errors

Fix:

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.

Mistake:

Calling recover() outside of a deferred function, where it always returns nil

Fix:

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 { ... } }()

Mistake:

Ignoring the error returned by the deferred Close() call, potentially losing data on writes

Fix:

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.

Best Practices

  • Place defer immediately after the resource acquisition call — this keeps the open/close pair visually together and prevents forgetting cleanup when adding new return paths
  • Use named return values when a deferred function needs to inspect or modify the return error, such as in transaction commit/rollback patterns
  • Extract loop bodies into helper functions when defer is needed per iteration — this ensures each defer runs at the right time instead of accumulating until the outer function returns
  • In servers, always wrap goroutine entry points with recover middleware to prevent one handler's panic from crashing the entire process
  • Only recover panics you expect (your own types) and re-panic everything else — swallowing unknown panics hides real bugs

Summary

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.

Practice Go with hands-on challenges

Learn go defer, panic, and recover hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Go with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.