Panic and Recover
What is panic?
Section titled “What is panic?”A panic stops normal execution of the current goroutine. The goroutine immediately stops running its current function, begins unwinding the call stack, and runs any deferred functions it encounters on the way out. If nothing intercepts the panic, the program crashes and prints a stack trace.
Panics happen in two ways:
- Runtime errors — the Go runtime triggers them automatically for nil pointer dereferences, out-of-bounds slice/array accesses, type assertion failures on non-interface types, and similar impossible operations.
- Explicit calls — you call
panic(value)with any value you like (commonly a string or an error).
panic("something that should never happen")panic(fmt.Errorf("invariant violated: %v", state))Both forms unwind the stack the same way. The only difference is what value recover() returns.
defer + recover
Section titled “defer + recover”recover() is a built-in function that intercepts a panic. It only works when called inside a deferred function — calling recover() outside a defer always returns nil.
The canonical pattern:
defer func() { if r := recover(); r != nil { // r is the value passed to panic() fmt.Println("recovered:", r) }}()When the deferred function calls recover() and the goroutine is panicking, recover() stops the panic, returns the panic value, and allows the deferred function to finish normally. After the deferred function returns, the function that panicked returns — not to where panic was called, but to its own caller with zero values for any named return variables (unless you set them in the deferred function).
A common use is converting a panic into a returned error at a package boundary:
func safeCall(fn func()) (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("recovered: %v", r) } }() fn() return nil}When to use panic
Section titled “When to use panic”Panic is not a general-purpose error-handling tool. The rule: use panic for programmer errors and impossible states — conditions that can only occur if the code itself is wrong, not because of bad input or environmental conditions.
Appropriate uses:
- Violated invariants — a state that should be structurally impossible has been reached, indicating a bug in the program.
- Package initialization failures —
init()cannot proceed because a required resource is missing or misconfigured. Panicking here is acceptable because the program cannot run correctly regardless. - Unreachable branches — marking a
defaultcase or exhaustive switch branch that should never execute (as documentation and a safety net).
Not appropriate:
- File not found, network timeout, invalid user input, missing database rows — these are expected runtime conditions that callers must handle. Return
(T, error)for all of these.
Library code has an extra obligation: a library should almost never panic on bad caller input. If an internal operation might panic (e.g., parsing with regexp.MustCompile), convert it to an error at the boundary before returning to the caller.
Panic vs error: a quick guide
Section titled “Panic vs error: a quick guide”| Situation | Use |
|---|---|
| Invalid user input | return error |
| File not found | return error |
| Nil pointer impossible by design | panic |
| Uninitialized required dependency | panic in init |
| Index out of range | panic (runtime) |
package main
import "fmt"
func safeDivide(a, b int) (result int, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("recovered panic: %v", r) } }() if b == 0 { panic("division by zero") } return a / b, nil}
func mustPositive(n int) int { if n <= 0 { panic(fmt.Sprintf("expected positive, got %d", n)) } return n}
func main() { result, err := safeDivide(10, 2) if err != nil { fmt.Println("error:", err) } else { fmt.Println("10 / 2 =", result) }
result, err = safeDivide(10, 0) if err != nil { fmt.Println("error:", err) } else { fmt.Println("10 / 0 =", result) }
fmt.Println(mustPositive(5))}Loading Go runtime (first run only, ~8 MB)…