Errors are values you return. Panic is the other failure path — it unwinds the stack, runs deferred functions, and terminates the goroutine if nothing catches it. You will see it. You should almost never reach for it.
What Causes a Panic
Most panics are bugs, not deliberate calls:
var p *Pod
fmt.Println(p.Name) // panic: nil pointer dereference
m := map[string]int{}
ch := make(chan int)
close(ch)
close(ch) // panic: close of closed channel
s := []int{1, 2, 3}
fmt.Println(s[5]) // panic: index out of range [5] with length 3
The runtime panics on its own. panic("boom") exists as a function, but you mostly hit it via these implicit paths.
When Calling panic Yourself Is OK
The legitimate cases are narrow:
init()failures — if a package can't initialize, panicking is correct. There's no caller to return an error to.- Programmer-error invariants —
panic("unreachable")in a switch default that should never hit, or amust*helper for tests / package-init regexes:
var portRE = regexp.MustCompile(`^\d+$`) // panics if the regex is malformed at startup
- Library code that genuinely cannot continue — and even there, prefer returning an error. Standard library
encoding/jsonreturns errors. So should you.
If a caller might want to handle the failure, return an error. Don't panic.
Recover
recover only does anything inside a deferred function, and it stops a panic from propagating:
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
return a / b, nil // panics if b == 0
}
Two things to notice: the recover lives in a defer, and the named return value err is what carries the failure back. Outside of defer, recover() always returns nil.
The legitimate uses of recover are narrow too:
- Boundary code — an HTTP server that doesn't want one bad request to kill the whole process. The standard library's
net/httpalready does this for you. - Goroutine supervision — top-level recover in a worker so one bad job doesn't take down the pool.
- Converting third-party panics to errors — when wrapping code you don't control.
If you're using recover in normal application logic, you're probably misusing it. The right move is "fix the bug that caused the panic," not "catch and continue."
What This Means in Practice
- A
nil pointer dereferencein production usually means a missingif x == nilcheck upstream, not a need for recover. panicpropagates across goroutines only by terminating the program — a panic in goroutine A is not catchable from goroutine B. Each goroutine that might panic needs its own deferred recover.deferruns even on panic, which is whydefer file.Close()is safe.
The Go proverb: "Don't panic." Use errors.