Panic Recovery in Go - Tutorial

Elliot Forbes Elliot Forbes ⏰ 5 Minutes 📅 May 10, 2022

👋 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:

  1. It checks to see if there is anything deferred within panickingPeter and executes these deferred statements.
  2. 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:

  1. We could place this at the top of our panickingPeter function - This would allow panickingPeter to explicitly decide how it should handle itself if the worst was to happen and a panic was caused.
  2. 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.