21.5 Converting Panics to Errors: The Pattern
Right, so you’ve decided you want to handle panics. Good for you. Most of the time, letting your program just explode and dump a stack trace to some poor user’s terminal is a pretty terrible user experience. It’s the programming equivalent of just walking away mid-conversation. Rude.
But you can’t just recover anywhere. That’s the first and most important thing to understand. The recover function only does anything useful when it’s called inside a deferred function. And not just any deferred function—one that’s running because a panic is currently unwinding the stack. Outside of that context, recover returns nil and does absolutely nothing. It’s a superhero that only works in its own specific comic book universe.
The canonical pattern for converting a panic into a regular error looks like this. Let’s break it down.
func mightPanic() (err error) {
// This deferred anonymous function is our safety net.
defer func() {
// Here's where the magic happens. Recover captures the panic.
if r := recover(); r != nil {
// Now we have to figure out what to do with 'r'.
switch x := r.(type) {
case string:
err = errors.New(x)
case error:
err = x
default:
// Fallback for some other panic type (e.g., int)
err = fmt.Errorf("panic: %v", r)
}
}
}()
// The actual function logic that might blow up goes here.
doTheDangerousThing()
// If we get here, everything was fine. Return the normal nil error.
return nil
}
Why Defer is Non-Negotiable
Think about the call stack. When a panic happens, it starts barreling back up the stack, immediately terminating any function in its path. The only thing that gets executed on the way out are deferred functions. They are the emergency crews that get to work as the building is evacuating. Your recover call has to be stationed inside one of these crews to have any chance of stopping the panic. Putting it in the normal flow of your function is like calling the fire department after your house has already burned down.
What Are You Even Recovering?
The value returned by recover() is whatever you passed to the panic() function. This is usually a string or an error, but the language spec doesn’t care. You can panic(42) or panic(MyStruct{}) if you’re a sociopath. This is why the type assertion in the example is so critical. You can’t just assume it’s an error. You have to handle the common cases and provide a sensible fallback for the weird ones. The worst thing you can do is just do err = fmt.Errorf("%v", r) for everything; you lose type information and the ability to neatly check for specific errors later.
The Naming and Scoping Trick
Did you notice the named return value err? That’s not an accident. Inside the deferred function, we’re modifying the return value of mightPanic itself. When the panic happens, the normal return path is skipped. By assigning to err in the deferred recover function, we ensure that our function still returns something meaningful to the caller, even though the code after doTheDangerousThing() never ran. If you used unnamed return values, you’d have a much harder time getting the converted error back to the caller. This is one of the few places where named returns are not just acceptable, but actively recommended.
The Biggest Pitfall: Re-panicking
Here’s a scenario: you recover a panic, but after looking at it, you realize it’s one you can’t or shouldn’t handle. Maybe it’s an unrecoverable “out of memory” error. The pattern for this is to simply re-panic. This is perfectly valid. You caught it, you logged it, you decided it was still fatal, and you let it continue its journey.
defer func() {
if r := recover(); r != nil {
log.Printf("Captured panic during dangerous operation: %v", r)
// Check if it's something we can't handle
if isUnrecoverable(r) {
log.Fatalf("Fatal error, cannot continue: %v", r)
// Or simply re-panic with the same value
panic(r)
}
// ... otherwise, convert it to an error
}
}()
The key takeaway? Panic and recover are not for everyday error handling. They’re for truly exceptional situations, often when you’re dealing with someone else’s code that you can’t change (like a library that panics) or when you’re writing code that could fail in such a catastrophic way that the only sane response is to bail out completely. Use them sparingly, and always, always wrap them in this pattern so you don’t just explode your user’s program without a word.