Sometimes, things go bad. There's no denying that worst cases will occur and request execution might have to be terminated completely. When it comes to your own code, you'll probably know when errors are returned and what this means for your request lifecycle, whether it should be continued or return an error message instead. This is the easy case, you'll get the error and can decide what to do with it. Your handler func could look like the following (very simplified) example:
package main
import (
"net/http"
)
func handleMyRequest(w http.ResponseWriter, r *http.Request) {
// Run some business logic
if err := doSomething(); err != nil {
w.WriteHeader(http.StatusInternalServerError)
_,_ = w.Write([]byte("Something went wrong"))
return
}
w.Write([]byte("Success"))
}
But this is not the only way exceptions are raised. There can be cases where libraries you use underneath have the habit to panic with errors they deem to be fatal. Of course, the thought behind writing a panic might not have been ill-intentioned, but then again, it's crashing your application. Sure, in today's serverless deployments your cloud provider will quickly fire up another container, but all other requests to your service just got terminated as well, database connections did not get cleaned up gracefully, it's a complete mess.
Luckily, we can create a function that will wrap our request handler, waiting for a panic to happen. In case something in your code, no matter
how deep down it is, decides to panic, you'll be able to recover, which also retrieves the argument passed to panic()
, and handle the
error.
package main
import (
"encoding/json"
"errors"
"net/http"
"runtime/debug"
)
// Handler func to wrap another handler and recover panics
func RecoverHandler(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, request *http.Request) {
// Defer recovery func
defer func() {
// Recover potential panic, if this returns nil, everything
// went as expected and there are no errors to be handled
recovered := recover()
if recovered == nil {
return
}
// We expect panic to return a real error,
// but we can also handle all other types
var err error
switch r := recovered.(type) {
case error:
err = r
default:
err = errors.New("unknown panic reason")
}
// Marshal an error response
jsonBody, err := json.Marshal(map[string]interface{}{
"error": err.Error(),
// This adds the current stack information
// so we can trace back which code panic'd
"stack": string(debug.Stack()),
})
if err != nil {
return
}
// Send JSON error response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write(jsonBody)
}()
// Invoke wrapped handler func
next.ServeHTTP(w, request)
}
}
The last thing to do is to wrap our initial handler in our newly-created recovery handler.
http.HandleFunc("/", RecoverHandler(handleMyRequest))
And that's it! With the addition of our relatively simple recovery handler, there's no way your service crashes due to a failing request.
I hope you enjoyed this short guide, maybe you can adopt a similar solution to your application, making it more resilient to unexpected failures 👍 If you've got any questions, suggestions, or feedback in general, don't hesitate to reach out on Twitter or by mail.