Menu

Go Panic and Recover: Runtime Panics and When to Panic

A panic stops normal execution and unwinds the stack, running deferred calls. Learn what causes panics, how recover in a deferred function stops one, the runtime error messages you will see, and when panicking is the right call.

This page includes runnable editors - edit, run, and see output instantly.

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

MessageCause
index out of range [5] with length 3slice, array or string index past the end
slice bounds out of range [:7] with capacity 5slicing past capacity
invalid memory address or nil pointer dereferencereading a field or calling through a nil pointer
assignment to entry in nil mapwriting to a map that was never created
interface conversion: interface {} is int, not stringsingle-value type assertion to the wrong type
integer divide by zerointeger division or modulo by 0 (floats give +Inf or NaN instead)
close of closed channel, send on closed channelchannel 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:

  1. a / b panicked.
  2. The deferred closure ran, and recover() returned the panic value (a runtime.Error).
  3. Unwinding stopped. safeDivide returned normally to main, with the named result err set 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 switch over your own enum reaches a case that cannot happen. Continuing would corrupt data.
  • A Must helper gets bad constant input. regexp.MustCompile, template.Must and uuid.MustParse wrap 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 calling os.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/json encoder), 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 main for a goroutine's panic. Each goroutine needs its own.
  • Swallowing all panics. Log with the stack (debug.Stack() from runtime/debug) and re-panic what you did not expect.
  • Using recover to 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.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED