How to Recover from Panics in Go
Before putting code out into the world, it's important to be as proactive as possible in handling any and all errors to ensure happy customers and restful nights.
We'll run through a quick example here to show what a panic looks like and how we can solve for them.
Stop! Panic Time!
So, here's our application. A simple divide function wrapped with a couple of log statements.
package main
import "fmt"
func main() {
fmt.Println("Started!")
fmt.Println(divide(2, 1))
fmt.Println("Done!")
}
func divide(numerator int, denominator int) int {
return numerator / denominator
}
When we run this code, the output looks like this:
Started!
2
Done!
In order to show what a panic looks like and how it behaves, we'll replace the denominator with a 0 which will cause a panic in our program's execution.
package main
import "fmt"
func main() {
fmt.Println("Started!")
fmt.Println(divide(2, 0))
fmt.Println("Done!")
}
func divide(numerator int, denominator int) int {
return numerator / denominator
}
When we run the updated application, we see the following output:
Started!
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.divide(...)
~/Code/go-opts/pkg/main.go:12
main.main()
~/Code/go-opts/pkg/main.go:7 +0x7b
exit status 2
A couple of things to point out in the output above is that we see:
- The error that caused the panic at the top (starting with panic:)
- The stack trace for the error to help find out where it occurred
This information will hopefully be helpful enough for you to figure out what is causing this issue.
Recovering
There's a pretty simple way to handle panics. All you need to do is have is have a defer block that uses the recover function to capture the errors.
What we're doing by using defer is saying that when this function is about to exit for normal or panic related reasons, that we always want this block of code to run.
So, since this block always runs, we can use the recover function to capture the error on the way out and handle it without having it cause our application to exit. It's basically the equivalent of a try/catch block in Java.
Here's what our new code will look like with this new error handling block:
package main
import "fmt"
func main() {
fmt.Println("Started!")
fmt.Println(divide(2, 0))
fmt.Println("Done!")
}
func divide(numerator int, denominator int) int {
defer func() {
if r := recover(); r != nil {
fmt.Println(r)
}
}()
return numerator / denominator
}
When we run this, our application prints out the following logs:
Started!
runtime error: integer divide by zero
0
Done!
You can see that even though our application was about to panic, that our recover function captured the error, allowed our function to complete and printed out the Done! log at the the end.
I hope this was helpful!
Addendum
I'll definitely admit that returning a zero on it's own may be misleading and hides the actual issue.
One option would be to add an error value in the set of return values and utilize named return parameters to override the value on the way out:
package main
import (
"fmt"
)
func main() {
fmt.Println("Started!")
fmt.Println(divide(2, 0))
fmt.Println("Done!")
}
func divide(numerator int, denominator int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = r.(error)
}
}()
return (numerator / denominator), nil
}
I'll dive into named parameters more in a future post, but what this does is allow you to set the error value and now other developers that are calling this function can see when an error occurs during this call allowing them to handle that however they see fit without causing the application to crash.
The result of this change looks like this:
Started!
0 runtime error: integer divide by zero
Done!