Go Panic, Recover, and Defer: Complete Guide

Go Panic, Recover, and Defer: Complete Guide
In the previous module, we discussed how Go handles standard errors as values. But what happens when an error is so severe that the program cannot possibly continue? Or what happens when you open a database connection and want to ensure it closes regardless of how the function finishes?
Go provides three unique keywords — Defer, Panic, and Recover — to manage function execution and handle exceptional states.
What Are Panic, Recover, and Defer in Go?
In Go, defer schedules a function call to run just before the surrounding function returns — ideal for resource cleanup. panic signals an unrecoverable error and begins unwinding the call stack. recover catches a panic inside a deferred function, allowing the program to log the error and continue running rather than crashing. Together they form Go's mechanism for safe, explicit error handling at the boundaries of your application.
A Different Philosophy
Resource Management: Defer
The defer keyword is one of Go's most elegant features. It schedules a function call to run immediately before the surrounding function returns. This is perfect for cleaning up resources like closing files or database segments.
The Stacked Nature of Defer
If you use multiple defer statements in a single function, they are executed in Last-In, First-Out (LIFO) order.
Fatal Failures: Panic
A panic stops the ordinary flow of control and begins panicking. When a function panics, its execution stops, any deferred functions are executed normally, and then the control returns to the caller, which also panics. This continues up the stack until the program crashes.
The Safety Net: Recover
recover is a built-in function that regains control of a panicking goroutine. It is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no effect.
If the current goroutine is panicking, a call to recover will capture the value given to panic and resume normal execution.
When to use what?
file.Close(), mu.Unlock()Use for any cleanup task to avoid resource leaks. It makes code readable by keeping the close logic near the open logic.
Initialization failuresOnly use when the application cannot fulfill its purpose, such as a missing critical dependency or a programmer error.
Web Server HandlersUse at the boundaries of your application (like a middleware) to prevent one bad request from crashing the entire server.
| Task / Feature | Error Handling | Panic/Recover |
|---|---|---|
| Frequency | Constant (every potentially failing func) | Rare (extraordinary circumstances) |
| Control Flow | Linear (if err != nil) | Stack-unwinding (immediate exit) |
| Use Case | Expected failures (Network, missing files) | Unexpected programmer errors (Nil pointers) |
Multiple Defers and LIFO Ordering
When multiple defer statements appear in one function, they run in Last-In, First-Out (LIFO) order — like a stack. This ordering is critical when you need to release resources in the reverse of the order you acquired them.
This mirrors the pattern used in real database connection management, which you can explore further in the Go database and SQL guide.
Defer with Named Return Values
One subtle but powerful feature is that a deferred function can read and modify named return values, even after the return statement has been evaluated.
This pattern is frequently used in library code to convert panics into ordinary error return values, keeping the library's public API clean and idiomatic.
Panic vs. Error: When to Use Which
Go's standard guidance from the Effective Go documentation is clear: panics should be rare and reserved for truly unrecoverable situations. For predictable failure modes — network errors, missing files, invalid user input — return an error value instead.
| Situation | Use |
|---|---|
| Missing required environment variable at startup | panic |
| Database query returns no rows | error |
| Nil pointer dereference bug | panic (caught by recovery middleware) |
| Invalid user-provided JSON | error |
For a deeper comparison of Go's error handling philosophy, see the Go error handling patterns guide.
Deferred Functions and Performance
defer has a small overhead because the runtime must record the deferred call in a list. In extremely hot paths (millions of calls per second), this can be measurable. The Go team's official blog post on defer, panic, and recover notes that since Go 1.14, the compiler can inline deferred calls in many common scenarios, making the overhead negligible for typical usage.
As a practical rule: always use defer for resource cleanup. Only optimize it away after profiling confirms it as a bottleneck — which is rare in real applications.
Using Recover in Web Middleware
The most common production use of recover is inside Go middleware patterns. A recovery middleware wraps every route handler so that a panicking handler returns a 500 response rather than crashing the server process. This is a critical pattern for any Go web application — revisit the middleware guide for a full implementation.
Next Steps
Now that you can manage the lifecycle of your functions and handle crashes, it is time to look at the bigger picture. In the next tutorial, we will explore Modules and Packages, where you'll learn how to organize your code into maintainable, shareable components.
Common Mistakes with Panic, Recover, and Defer
1. Using panic for expected errors
panic is for unrecoverable programmer errors — nil dereferences, index out of bounds, invariant violations. Expected runtime errors (file not found, network timeout) should be returned as error values. Using panic for control flow is an anti-pattern in Go.
2. Forgetting defer runs in LIFO order
Multiple defer statements in the same function execute in last-in, first-out order — the last defer registered runs first. This matters when closing resources: open A, open B, defer close B, defer close A ensures correct teardown order.
3. Deferring in a loop
defer inside a loop registers a new deferred call on every iteration, but none execute until the function returns — not when each loop iteration ends. For per-iteration cleanup, use a helper function or an explicit close call inside the loop.
4. recover() not in a deferred function
recover() only works inside a deferred function called directly during a panic unwind. Calling it outside of defer always returns nil. Wrap your recovery logic in defer func() { if r := recover(); r != nil { ... } }(). See the Go spec on handling panics.
5. Swallowing panics without logging
A bare recover() that does nothing hides serious bugs. Always log the recovered value and consider whether the program state is still valid before continuing.
Frequently Asked Questions
Should I ever use panic in library code?
Generally no. Library functions should return errors so callers can decide how to handle them. Panicking in a library forces every caller to use recover defensively. The standard library uses panic internally in a few places (e.g. encoding/json for performance), but converts them back to errors before returning to callers.
What does defer do to return values?
A named return value can be modified by a deferred function. defer func() { err = fmt.Errorf("wrapped: %w", err) }() is a common pattern to add context to returned errors. Anonymous return values cannot be modified by defer.
When is recover appropriate?
The main use case is at server boundaries — an HTTP handler recovering from a panic to return a 500 error instead of crashing the entire server. The Go blog on defer, panic, and recover is the canonical reference.
