Panic Recovery in Go - Tutorial
👋 In this tutorial, we are going to be covering the concept of panic recovery in Go.
Panics in Go
Panics are something I very rarely use within my day-to-day Go application development. Most of the time, catching an error from whatever I’m calling and then logging it out and possibly emitting a metric that lets me know how often this is happening is the preferential option.
Panics are not meant to be comparable to exceptions in other languages - by this I mean that you will not see Go developers use them the same way PHP developers, Java Developers or JavaScript developers might use them within their own language domains.
They are instead a last-ditch mechanism that is built-in to the language that allows us to handle error cases that we aren’t to handle in a graceful fashion.
Video Tutorial
If you would prefer to watch the video version of this tutorial you can do so here:
Cascading Panics
When a panic
is called within one of our Go apps, it will effectively run back up the call stack until it’s either caught, or the program exits.
package main
import "fmt"
func sillySusan() {
fmt.Println("silly susan called")
panickingPeter()
fmt.Println("silly susan finished")
}
func panickingPeter() {
fmt.Println("panicking peter called")
panic("oh no")
fmt.Println("panicking peter finished")
}
func main() {
fmt.Println("cascading panics")
sillySusan()
}
Note - If your name is either Susan or Peter then I’d like to apologize in advance. The choice of names was arbitrary for alliteration purposes 🤓
In the above example, we have a chain of functions being called. Our application starts its’ execution from the main
func. This in turn calls sillySusan
which then calls panickingPeter
.
Now, when panickingPeter
calls the builtin panic
function, the function does 2 things:
- It checks to see if there is anything deferred within
panickingPeter
and executes these deferred statements. - It then terminates.
At the point at which it terminates, the parent function, in this case sillySusan
is effectively going to treat the call to panickingPeter
as a call to panic
and then walk through the above 2 steps again.
This cycle carries on indefinitely until the application is terminated. We can see this is the case from the output of the above program:
cascading panics
silly susan called
panicking peter called
panic: oh no
goroutine 1 [running]:
main.panickingPeter()
/tmp/sandbox773545457/prog.go:15 +0x65
main.sillySusan()
/tmp/sandbox773545457/prog.go:9 +0x5b
main.main()
/tmp/sandbox773545457/prog.go:22 +0x57
Program exited.
You should note that both the silly susan finished
and panicking peter finished
print line statements are never executed.
Catching Panics
Thankfully, Go does have a built-in function that allows us to stop this cascading up the call stack and terminating our production applications.
This built-in function is recover
and we can use this within a deferred statement.
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
Now, we have a few options with where to place this:
- We could place this at the top of our
panickingPeter
function - This would allowpanickingPeter
to explicitly decide how it should handle itself if the worst was to happen and a panic was caused. - We could place this in the
sillySusan
function - This could potentially be useful in situations where we want to gracefully handle panics for a wider range of functions that we don’t necessarily have control over.
Let’s take a look at option 2 in a full code example:
package main
import "fmt"
func sillySusan() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Susan recovered from the panic")
fmt.Println("silly susan handles the panic gracefully")
}
}()
fmt.Println("silly susan called")
panickingPeter()
fmt.Println("silly susan finished")
}
func panickingPeter() {
fmt.Println("panicking peter called")
panic("oh no")
fmt.Println("panicking peter finished")
}
func main() {
fmt.Println("cascading panics")
sillySusan()
}
When we run this now, we should see that sillySusan
is able to effectively recover from the panic caused by panickingPeter
.
Within the defer func
we are able to log out that we’ve been able to recover and at this point we then have options on how we want to handle the panic. In this case, we just want to log out that we’ve handled it, but if this was a production-like environment then we may want to emit a metric to alert upon or maybe send the error to rollbar.
For additional context, we can pass additional contextual information to our panic
function which we can then access within our recover logic:
package main
import "fmt"
type SomeStruct struct {
message string
}
func sillySusan() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Susan recovered from the panic")
fmt.Printf("%+v\n", r)
fmt.Println("silly susan handles the panic gracefully")
}
}()
fmt.Println("silly susan called")
panickingPeter()
fmt.Println("silly susan finished")
}
func panickingPeter() {
fmt.Println("panicking peter called")
panic(SomeStruct{message: "hello from peter panic"})
fmt.Println("panicking peter finished")
}
func main() {
fmt.Println("cascading panics")
sillySusan()
}
If we were to then run this we would see the following output:
cascading panics
silly susan called
panicking peter called
Susan recovered from the panic
{message:hello from peter panic}
silly susan handles the panic gracefully
Program exited.
Recovery Middleware
Let’s take a look at the second potential option for placing recovers in a real production library.
func (h recoveryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
defer func() {
if err := recover(); err != nil {
w.WriteHeader(http.StatusInternalServerError)
h.log(err)
}
}()
h.handler.ServeHTTP(w, req)
}
In the above code, we have a recoveryHandler
from the (github.com/gorilla/handlers)[https://github.com/gorilla/handlers] library which showcases the use of a deferred function that calls recover to ensure that panics in the handler functions are gracefully handled:
r := mux.NewRouter()
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
panic("Unexpected error!")
})
http.ListenAndServe(":1123", handlers.RecoveryHandler()(r))
Conclusion
Awesome, so in this tutorial, we looked at how you can effectively build in recovery mechanisms into your Go applications that help to prevent panics in your code from cascading up the call stack.