21.1 panic: Signaling Unrecoverable Errors
Right, let’s talk about panic. You’ve probably seen it. The program stops, a bunch of red text vomits all over your terminal, and you feel a brief moment of pure, unadulterated shame. Don’t. A panic is how Go tells you, in no uncertain terms, that something has gone so fundamentally sideways that it cannot possibly continue executing your code with any integrity. It’s the runtime’s version of throwing its hands up and saying, “I’m out. You deal with this.”
Think of it as the opposite of returning an error. An error is for when something goes wrong in the world—a file isn’t found, a network call fails, user input is gibberish. These are expected failures. A panic is for when something goes wrong in the program—a bug so severe that the very logic of the application is broken. It’s for “this should never, ever happen” scenarios.
What Actually Happens During a Panic
When you call panic, either explicitly or by doing something daft like accessing a slice out of bounds, Go immediately stops the normal execution of the current function. It’s not a gentle exit. It then starts unwinding the stack, running any defer statements it finds on the way out. This is crucial. defer is your life raft here. Only after all defers have run in the current goroutine does the panic finally crash the program, printing the infamous stack trace.
Here’s the lifecycle in action. Watch the order of the prints.
func main() {
fmt.Println("Start of main")
deeplyNestedFunction()
fmt.Println("This will never print") // The panic unwinds past us
}
func deeplyNestedFunction() {
defer fmt.Println("Defer in deeplyNestedFunction - cleaning up!")
fmt.Println("About to panic...")
panic("oh no, everything is terrible")
fmt.Println("This is unreachable code")
}
Running this will give you:
Start of main
About to panic...
Defer in deeplyNestedFunction - cleaning up!
panic: oh no, everything is terrible
(goroutine and stack trace details follow...)
See? The defer still ran. This is your mechanism for emergency cleanup—closing network connections, unlocking mutexes, you name it—even when the world is ending.
You Should Almost Never Call panic Yourself
I’m serious. The number of legitimate reasons to manually panic in application code is vanishingly small. If you can anticipate a condition, you should be handling it with a returned error. It’s a control flow mechanism, not an error handling one.
The standard library uses it internally for genuine “this is impossible” cases. A classic example is doing something like this:
func thisIsFine() {
var mySlice []int // nil slice
fmt.Println(mySlice[42]) // This will panic: runtime error: index out of range [42] with length 0
}
Accessing mySlice[42] is a programmer error. The code is fundamentally broken. The runtime should panic because there’s no sane value to return; continuing would corrupt data.
If you find yourself writing panic("invalid user input"), you’re doing it wrong. Return an error instead. If you’re writing a library and you panic because a caller passed a nil pointer, you’re being a bad library author. Your library’s bugs shouldn’t crash my application. Return an error.
The One Good Reason: Initialization
There’s one acceptable, idiomatic place to call panic yourself: package initialization (init() functions or var declarations). If your package cannot possibly be used if a certain setup step fails—like parsing a vital config template that’s hard-coded—then panicking is appropriate. The application cannot run, so it should not run.
var criticalConfig Template
func init() {
var err error
criticalConfig, err = template.New("abc").Parse("{{.ThisMustWork}}")
if err != nil {
// This is a programming error. The template is hard-coded and wrong.
// The app is fundamentally broken and cannot start. This warrants a panic.
panic(fmt.Sprintf("failed to parse critical template: %v", err))
}
}
If your hard-coded template has a syntax error, the program deserves to die immediately. There’s no recovering from that.
Recover: The Emergency Brake
Ah, but what if you don’t want the program to crash? Enter recover. It’s a built-in function that regains control of a panicking goroutine. It’s not for general error handling. It’s an emergency brake, and you yank it inside a defer.
You use it in strategic places where you need to prevent a panic from bubbling up and crashing the entire process. A web server server shouldn’t crash completely because one request handler panicked. A goroutine you spun off to do background processing shouldn’t take the whole app down with it.
Here’s the pattern:
func mightPanic() {
defer func() {
if r := recover(); r != nil {
// r is the value passed to panic()
fmt.Println("Recovered from panic:", r)
// You can do cleanup here, log the event, etc.
// The panic is stopped, and this function returns normally.
}
}()
fmt.Println("This is fine.")
panic("internal server error: existential dread")
fmt.Println("This won't run.")
}
func main() {
mightPanic()
fmt.Println("Main continues happily because we recovered.")
}
The Sharp Edges of Recover
recover is powerful but weird. You have to know its quirks or you’ll shoot yourself in the foot.
It only works in a
defer. Callingrecoverin the normal flow of a function does absolutely nothing. It must be inside a deferred function. This is by design; it forces you to structure your panic handling as cleanup.It only catches panics from the same goroutine. You can’t catch a panic from another goroutine. If you
go mightPanic(), that goroutine will crash, and your main goroutine won’t know about it until it’s too late. This is why you see thedeferandrecoverpattern at the top of many goroutine functions.You can re-panic. Sometimes, you catch a panic, log it, but realize you can’t actually handle it. You can just call
panic(r)again inside thedeferto keep the panic going up the stack. This is useful if you’re recovering at a low level to add contextual information but still need to signal failure.
The bottom line: Use panic to signal catastrophic, programmatic failure. Use recover sparingly, only at well-defined synchronization points (like a goroutine’s start or a web request boundary) to convert a panic into a log message and an error. And for the love of all that is good, stop trying to use it like a try/catch block. That’s not its job.