panic, recover and When to Use Them
panic stops normal flow for a bug you cannot handle; recover, inside a deferred function, turns it back into an error at a boundary.
defer func() { if r := recover(); r != nil { /* handle r */ } }()A panic stops the normal flow of a program. The running function stops where it is. Its deferred functions run, then its caller's, and so on up the stack. If nothing stops the panic, the program prints the panic value and a stack trace to standard error and exits with status 2.
What panics
The runtime panics on bugs: indexing past the end of a slice, dereferencing a nil pointer, writing to a nil map, dividing an integer by zero. You can panic yourself with panic(v), which takes a value of any type.
When to use it
Panic for programmer error, meaning a state that proves the code is wrong rather than that the input was bad. Bad input, a missing file, a refused connection are ordinary failures: return an error. A library should almost never let a panic cross its own boundary.
recover
Called inside a deferred function, recover() returns the panic value and stops the unwinding. The panicking function then returns normally, so pair recover with named results to set an error. Outside a deferred function, and when there is no panic, recover returns nil. Since Go 1.21 panic(nil) is replaced by a *runtime.PanicNilError, so recover no longer returns nil for it.
At a boundary
Recover earns its place at a boundary that handles many independent units of work: one request, one job, one message. Wrap each unit so a panic ends that unit and gets reported, instead of taking everything down. Log the value and a stack trace, then return a failure. Recovering and carrying on as if nothing happened hides real bugs.
Example
package main
import "fmt"
// safeDivide turns a panic into an ordinary error. The results are named, so
// the deferred function can set err after the panic has happened.
func safeDivide(a, b int) (result int, err error) {
defer func() {
// recover returns the value passed to panic, or nil if nothing panicked.
if r := recover(); r != nil {
err = fmt.Errorf("safeDivide(%d, %d): %v", a, b, r)
}
}()
return a / b, nil
}
func main() {
n, err := safeDivide(10, 2)
fmt.Println(n, err)
// Dividing by zero panics. The recover above catches it, so main continues
// and result keeps its zero value.
n, err = safeDivide(10, 0)
fmt.Println(n, err)
}
// Output:
// 5 <nil>
// 0 safeDivide(10, 0): runtime error: integer divide by zeroWhen to use it
- Stop one failing background job from ending a worker process that is handling others.
- Fail loudly at startup when a required template cannot be compiled, because nothing can work afterwards.
- Turn an unexpected nil pointer in a request handler into a logged failure and a 500 response.
More examples
The four panics you will meet first
These four account for most panics in real Go programs, and each one has a cheap fix: check the length before indexing, create the map with make, check a pointer against nil, check the divisor. Knowing the exact wording helps, because the panic message in a log is often all you get. The last two lines are worth remembering: reading from a nil map is legal, only writing panics.
// Each of these is a bug in the code, not a bad input. None of them should be
// recovered from: fix the code instead.
var s []int
_ = s[0] // panic: runtime error: index out of range [0] with length 0
var m map[string]int
m["a"] = 1 // panic: assignment to entry in nil map
var p *int
_ = *p // panic: runtime error: invalid memory address or nil pointer dereference
a, b := 1, 0
_ = a / b // panic: runtime error: integer divide by zero
// A nil map can be read from, which is why the write is the surprise. Reading
// gives the zero value and does not panic.
fmt.Println(m["a"], len(m)) // 0 0A boundary that keeps one bad job from stopping the run
This is the shape of every panic boundary: one small wrapper per unit of work, a recover that converts the panic into an error, and a caller that reports it and moves on. The same wrapper around an HTTP request is covered in go-middleware. Worth knowing: Go's own HTTP server already recovers panics in handlers so the process survives, but it only logs and drops the connection, so your own boundary is what sends a proper failure response and records the error where you will see it.
package main
import "fmt"
// runJob calls job with its own recover, so a panic ends that job and becomes
// an error. The named result err is how the deferred function reports it.
func runJob(name string, job func()) (err error) {
defer func() {
if r := recover(); r != nil {
// In a real service, also log a stack trace here with
// debug.Stack() from runtime/debug.
err = fmt.Errorf("job %s panicked: %v", name, r)
}
}()
job()
return nil
}
func main() {
jobs := []struct {
name string
fn func()
}{
{"resize", func() {}},
{"thumbnail", func() { panic("missing image data") }},
{"notify", func() {}},
}
for _, j := range jobs {
if err := runJob(j.name, j.fn); err != nil {
fmt.Println("error:", err)
continue
}
fmt.Println("ok:", j.name)
}
}
// Output:
// ok: resize
// error: job thumbnail panicked: missing image data
// ok: notifyRecovering, inspecting, and re-panicking
recover returns any value, so the first job after recovering is to work out what you caught. A type assertion plus errors.Is identifies your own signal; everything else is re-panicked, which keeps real bugs visible instead of swallowing them. Use this sparingly: it is justified when an error would otherwise have to be threaded through dozens of recursive calls, and the panic never escapes the package.
package main
import (
"errors"
"fmt"
)
var ErrParse = errors.New("parse failed")
// parse uses panic for a shortcut out of deep recursive code, a pattern the
// standard library uses in a few parsers. The panic value is an error, so the
// recover can tell it apart from a real bug.
func parse(ok bool) (err error) {
defer func() {
r := recover()
if r == nil {
return
}
// Only our own signal is converted. Anything else is a real bug and
// must keep going up.
if e, isErr := r.(error); isErr && errors.Is(e, ErrParse) {
err = e
return
}
panic(r)
}()
if !ok {
panic(fmt.Errorf("line 12: %w", ErrParse))
}
return nil
}
func main() {
fmt.Println(parse(true))
fmt.Println(parse(false))
}
// Output:
// <nil>
// line 12: parse failed
Discussion