What a panic looks like
A panic stops the current function, runs its deferred calls, then does the same in its caller, and so on up the stack. If it reaches the top of the goroutine, the program crashes.
This program exits with status 2. The output is:
before
deferred in main: still runs
panic: runtime error: index out of range [5] with length 3
goroutine 1 [running]:
main.main()
/tmp/main.go:11 +0x...
exit status 2
The deferred call ran before the crash report. The trace names the goroutine, the function and the line, which is usually enough to find the bug.
Common runtime panics
| Message | Cause |
|---|---|
index out of range [5] with length 3 | slice, array or string index past the end |
slice bounds out of range [:7] with capacity 5 | slicing past capacity |
invalid memory address or nil pointer dereference | reading a field or calling through a nil pointer |
assignment to entry in nil map | writing to a map that was never created |
interface conversion: interface {} is int, not string | single-value type assertion to the wrong type |
integer divide by zero | integer division or modulo by 0 (floats give +Inf or NaN instead) |
close of closed channel, send on closed channel | channel misuse |
all goroutines are asleep - deadlock! | every goroutine blocked (a fatal error, not a panic) |
Each of these is a bug in the program, not a condition to handle. The fix is a bounds check, a nil check, a make, or a comma-ok assertion, not a recover.
Recovering
recover() stops a panic. It only works when called directly inside a deferred function, because deferred functions are the only code that runs while a panic unwinds.
Output:
5 <nil>
0 recovered: runtime error: integer divide by zero
program continues
What happened in the second call:
a / bpanicked.- The deferred closure ran, and
recover()returned the panic value (aruntime.Error). - Unwinding stopped.
safeDividereturned normally tomain, with the named resulterrset by the closure.
The named result is what lets the deferred function hand an error back. Without one, the function returns its zero values. The defer page covers how deferred closures modify results.
recover() returns nil when there is no panic, so the if r != nil check makes the deferred function harmless on the normal path. Called outside a deferred function, or in a function called by the deferred function, recover returns nil and does nothing.
panic with your own value
panic takes any value. An error or a string is typical.
Recover what you expect and re-panic anything else. Swallowing every panic hides real bugs.
Since Go 1.21, panic(nil) is turned into a *runtime.PanicNilError, so recover() returning nil now reliably means "no panic".
Panics in goroutines
recover only catches panics in its own goroutine. A panic in any goroutine without a recover kills the whole process, including main and every other goroutine.
The two worker lines can print in either order; main finished always comes last. A defer recover() in main would not have saved the program from the second worker. This is why HTTP servers recover per request: net/http recovers panics in each handler goroutine, logs them, and closes that connection, so one bad request does not take the server down.
Some failures are fatal errors, not panics, and cannot be recovered at all: concurrent map writes, running out of memory, and the deadlock detector's all goroutines are asleep.
When panicking is the right call
Go's general rule: return errors for anything that can go wrong at run time, and panic only for programmer mistakes. Concretely, panic is appropriate when:
- An invariant is broken. A
switchover your own enum reaches a case that cannot happen. Continuing would corrupt data. - A
Musthelper gets bad constant input.regexp.MustCompile,template.Mustanduuid.MustParsewrap a function returning an error and panic on failure. Use them for values known at compile time, typically package-level variables, where a failure means the source code is wrong:
var emailRE = regexp.MustCompile(`^[^@\s]+@[^@\s]+$`)
- Startup cannot continue. Missing required configuration in
main. Even here, printing the error and callingos.Exit(1)is often cleaner than a stack trace.
Panic is the wrong tool for:
- Expected failures: invalid user input, a missing file, a timeout. Return an
error; see error handling. - Control flow: using panic and recover as exceptions across a large call tree makes code hard to follow. The standard library does this internally in a couple of places (the
encoding/jsonencoder), always recovering before returning, so no panic escapes the package. - Library APIs: a library that panics on bad input forces every caller to add recovers. Return an error.
Common mistakes
- Calling recover outside a deferred function. It returns
nil. - Recovering in
mainfor a goroutine's panic. Each goroutine needs its own. - Swallowing all panics. Log with the stack (
debug.Stack()fromruntime/debug) and re-panic what you did not expect. - Using
recoverto handle nil map writes or out-of-range indexes. Fix the bug instead.
Frequently Asked Questions
What is a panic in Go?
A run-time failure that stops the normal flow of the current goroutine. Go runs the deferred calls of each function on the stack, from the innermost outward, and if nothing recovers, the program prints the panic value and a stack trace and exits with status 2. Panics come from bugs (index out of range, nil pointer dereference, nil map write) or from an explicit panic(v) call.
How do you recover from a panic in Go?
Call recover() inside a deferred function: defer func() { if r := recover(); r != nil { ... } }(). It returns the value passed to panic and stops the unwinding, so the function that deferred it returns normally to its caller. Called anywhere else, recover returns nil and does nothing.
Can I recover a panic from another goroutine?
No. recover only stops a panic in the goroutine where it runs. A panic in a goroutine you started, with no recover inside that goroutine, crashes the whole program. Each goroutine that might panic needs its own deferred recover.
When should I use panic instead of returning an error?
For bugs and impossible states, not for expected failures. Bad input, missing files and network errors are errors. Panic is reasonable when an invariant is broken, when a Must helper is given a constant that should always be valid (regexp.MustCompile), or when the program cannot start at all. Libraries should not let panics escape their public API.