21.3 recover: Catching a Panic in a Deferred Function
Right, so you’ve met panic. It’s the language’s built-in fire alarm, and it’s meant for genuine catastrophes, not your average Tuesday. But sometimes, even in a well-tested system, a catastrophe happens. Maybe a third-party API sends back nil where you expected a complex data structure. Maybe you divided by a user-supplied value that, against all odds, was zero. When that panic rips through your call stack, shutting down everything in its path, you might want a safety net. That’s where recover comes in.
Think of recover as your emergency parachute. It’s not something you use in the middle of your normal control flow. Its one and only job is to stop the propagation of a panic and give you a chance to log what happened, clean up any resources, and maybe—maybe—gracefully degrade functionality instead of just cratering the entire process. It’s a last-ditch tool, and you have to use it correctly or it’s utterly useless.
The One Rule: recover Only Works Inside Defer
This is the most important thing to understand, and it’s the thing everyone gets wrong at least once. You can’t just slap a recover() call anywhere and expect it to work. It only has any effect when it’s called inside a deferred function.
Why? Because defer is the mechanism that ensures a function runs on the way out of a scope, even if that scope is being exited due to a panic. The runtime, as it’s unwinding the stack during a panic, will execute any deferred functions it comes across. It’s only in this specific context—inside a function that’s running because of the panic—that recover is empowered to do its job.
Here’s the canonical, correct pattern. You’ll see this everywhere, and for good reason:
func mightPanic() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
// Here you'd add your logging and error handling logic.
}
}()
// This is the dangerous code that might cause a panic.
fmt.Println("This might panic...")
panic("oh no, I panicked!")
}
func main() {
fmt.Println("Calling mightPanic...")
mightPanic()
fmt.Println("Program continues normally thanks to recover.")
}
Running this will print:
Calling mightPanic...
This might panic...
Recovered from panic: oh no, I panicked!
Program continues normally thanks to recover.
Without that defer and recover, the panic("oh no, I panicked!") would crash the program and the last line in main would never execute.
What You Actually Get Back
The recover function returns an interface{}. If it was called during an active panic, it returns the value that was originally passed to the panic call. If there was no active panic, it simply returns nil. That’s why you always see it used in an if r := recover(); r != nil check.
This value can be anything: a string, an error, a custom struct, an int (please don’t). It’s a classic Go interface: you’ll need to type assert it to do anything useful with it beyond logging.
func handleDifferentPanics() {
defer func() {
if r := recover(); r != nil {
switch v := r.(type) {
case string:
fmt.Printf("Recovered a string panic: %s\n", v)
case error:
fmt.Printf("Recovered an error panic: %v\n", v)
default:
fmt.Printf("Recovered an unknown panic type: %v (%T)\n", v, v)
}
}
}()
// Uncomment one of these to test:
// panic("I'm a string")
// panic(errors.New("I'm an error"))
panic(42) // Why would you do this? Don't do this.
}
The Gotchas and Rough Edges
This power comes with some serious caveats. The designers made choices here, and some of them are… interesting.
Gotcha #1: You Only Recover from the Current Goroutine. This is a big one. A recover in one goroutine cannot catch a panic happening in a different, concurrently running goroutine. If a goroutine panics and you didn’t defer a recover inside that specific goroutine, the whole program crashes. This is the number one reason people see their concurrent Go programs mysteriously vanish.
func main() {
// This recover in main does NOTHING for the panic in the goroutine.
defer func() {
if r := recover(); r != nil {
fmt.Println("This will never print.", r)
}
}()
go func() {
panic("I'm crashing in my own goroutine!")
}()
time.Sleep(1 * time.Second) // Give the goroutine a moment to explode.
fmt.Println("You will probably not see this line.")
}
The correct way is to put the recovery mechanism inside the goroutine itself.
Gotcha #2: Recovery Resumes Execution After the Defer. After a recover successfully handles a panic, execution does not resume from the point of the panic. How could it? The stack was unwound. Instead, it resumes execution in the function that contained the deferred recover, right after the deferred block. In the first example, execution continued in main after the call to mightPanic() finished (abnormally). The function where the panic occurred is still dead and gone.
Gotcha #3: You Can Recover and Then Panic Again. Sometimes you catch a panic, realize you can’t actually handle it meaningfully, and decide the right thing to do is to re-panic. This is a valid pattern. You might log the error, clean up some resource, and then re-initiate the panic. You can even change the value you panic with.
func sophisticatedRecover() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Logging the initial panic:", r)
// Do some cleanup...
// But then we decide this is still fatal.
panic(fmt.Sprintf("re-panicking after cleanup: %v", r))
}
}()
panic("original problem")
}
The bottom line is this: recover is a crucial tool for building robust applications, but it’s not a catch-all for bad error handling. Your first line of defense should always be explicit error return values. Use panic and recover for the truly unexpected, the “this should never happen” scenarios. And when you do use it, remember the rules: defer first, recover inside, and handle each goroutine’s demise individually.