Go Panic & Recover: Don't Make These Mistakes
While recover can help a service bounce back from a panic, if you don't fully grasp the root of the problem, it might not stop a crash caused by panic.
You know how using panic and fatal errors is usually seen as bad coding? Well, sometimes devs run into errors that can cause panic without even meaning to.
So let me give you a quick rundown on recover and panic, and show you how to bounce back from panic if you’re new to this stuff. We’ll also chat about some common mix-ups and slip-ups devs make because they don’t quite get the whole picture.
1. Combo: Panic, Recover and Defer
First up, let’s demystify what ‘panic
’ really does: When a panic occurs, it disrupts the regular flow of your program.
Imagine a function F
calls panic()
, it stops its regular activities, executes any deferred functions it may have, and then returns to its caller.
Because deferred functions still operate even during a panic
, it’s important to use defer
along with recover
, they work together to manage these unexpected situations effectively.
Oh, and if you wanna dive deeper into defer
and how it works, check out my article, “Go Secret — Defer: What you know about DEFER in Go is not enough!”. It’s got all the juicy details.
To give you a practical scenario where ‘recover’ saves the day, let’s consider the following code snippet:
func panicCode() {
var x *int
*x += 1
}
func main() {
defer func() {
if panicInfo := recover(); panicInfo != nil {
fmt.Println(panicInfo)
}
}()
panicCode()
}
// runtime error: invalid memory address or nil pointer dereference
By adopting this approach to handle panics, our application can recover gracefully, without messing up the rest of the goroutine. This keeps the program running smoothly, ensuring a seamless user experience.
2. Why does my code still panic?
If you put recover
in the wrong place, it won’t do its job and your service may still have issues. So, make sure you know where and how to use recover to make it work well for you.
a. Panic even deferred recover
It’s good practice to use defer with recover, but remember, the recover()
function should be inside the function that’s going to handle the panic.
Here’s what I mean:
// BAD
func main() {
defer recover()
panicCode() // <-- crash
}
// MUCH BETTER
func handlePanic() {
if panicInfo := recover(); panicInfo != nil {
fmt.Println(panicInfo)
}
}
func main() {
defer handlePanic()
panicCode()
}
b. Handle panic of child goroutine
Recover works fine in a deferred function, but it won’t help if there’s a panic in a goroutine started by that function.
func main() {
defer func() {
if panicInfo := recover(); panicInfo != nil {
fmt.Println(panicInfo)
}
}()
go panicCode()
time.Sleep(10 * time.Second) // simulate a web-server
}
That can cause that specific goroutine to stop, and maybe even bring your whole service down, even if you have more recover calls elsewhere.
“I’m using a library with goroutines and can’t modify it. How can I handle this?”
Well, you can wrap the library function in a goroutine and include a recover call like this:
go func() {
defer handlePanic()
panicCode()
}()
I’ve faced this issue, but usually in a roundabout way, popular web servers like Gin or Iris have built-in tools to handle panics. They’ll stop a request and return a ‘500 Internal Server Error’.
When you’re handling user requests with multiple goroutines, a panic in just one of them can bring down your entire server, leaving it unresponsive, even Gin, Echo, Iris,… won’t be able to save you in those cases.
3. Recover stack trace
When panic happens, the stack trace gives us clear info about what went wrong and where it happened:
But when you use recovery, you often just get a single line like this:
runtime error: invalid memory address or nil pointer dereference
Don’t worry though, you can get more details using the runtime/debug
package.
By tweaking our handlePanic
function, we can get a clearer picture of the issue, let’s address this minor obstacle and resolve it quickly:
import "runtime/debug"
func handlePanic() {
if panicInfo := recover(); panicInfo != nil {
fmt.Println("panic:", panicInfo)
debug.PrintStack()
}
}
If you’re looking to improve your error-handling, you might consider using an external library. From a recent project of mine, I can suggest either github.com/pkg/errors or github.com/go-errors/errors.
Just a quick note, pkg/errors is no longer actively developed, but it can still be very helpful for robust error management in your app.
These have been my own observations dealing with panic, recover, and defer. If you have more thoughts or advice to add, I’m eager to hear and share them.