Recovering from Panics
The Fundamentals middleware lesson already covered the shape:
func(http.Handler) http.Handler, a closure that runs code around a call to
next.ServeHTTP. Part 2 built the CRUD handlers this middleware will wrap.
This lesson doesn't re-teach either — it applies the pattern to a problem
those handlers can't solve themselves: what happens when one of them panics.
What a panic does to a bare net/http server
Go's net/http server already recovers panics per request — a panicking
handler doesn't crash the whole process. But the default behavior is blunt:
the connection is closed, the client sees a broken connection or an abrupt
EOF, and nothing resembling an HTTP response goes out. No status code, no
body, no chance for the server to log what happened in a structured way. For
an API, that's a bad client experience and a worse debugging experience.
This is exactly the kind of problem middleware is built for. A single
handler could wrap itself in defer/recover, but that means remembering
to do it in every handler, forever, including the one someone adds next
year without reading this lesson. Recovery belongs at the edge, wrapping
every handler in the chain, so the guarantee holds no matter what runs
underneath — a lookup on a nil map, a slice index past its length, a bad
type assertion, anything.
defer and recover(), and why order matters
The mechanics are two stdlib primitives working together:
func Recover(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
defer schedules a function to run when the enclosing function returns —
normally or via a panic unwinding the stack. recover() only does
anything useful when called directly inside a deferred function: it stops
the unwinding and returns the value passed to panic, or nil if there was
no panic in progress.
The ordering here is not cosmetic. The defer statement has to execute
before next.ServeHTTP(w, r) is called, because a defer only protects
code that runs after it's registered. If you called next.ServeHTTP first
and deferred the recovery afterward, the deferred statement would never be
reached — the panic would already be unwinding past it. Writing the defer
first is what puts the safety net under next before it has a chance to
fall.
When recover() returns non-nil, a panic happened somewhere inside next.
The handler responds 500 Internal Server Error via http.Error — a
generic message, deliberately, since the client has no business seeing
whatever string or error value the panic carried. When recover() returns
nil, next finished normally and the deferred function does nothing.
Same-goroutine only
recover() catches panics only in the goroutine where it's called. If a
handler spawns its own goroutine — say, to do work after the response is
sent — and that goroutine panics, this middleware's recover() won't see
it; that panic still crashes the process. Recover protects the request's
own goroutine, which is where next.ServeHTTP runs directly, but nothing
further.
What real middleware adds
Responding 500 is the minimum bar. Production recovery middleware
typically also logs the recovered value and a stack trace (via
runtime/debug.Stack()) before responding, so the panic is visible in logs
even though the client only ever sees a generic error. That's a natural
next step once logging middleware exists, but it's orthogonal to the
recover/respond mechanics this lesson covers.
There is also one panic value you must deliberately not handle:
if rec := recover(); rec != nil {
if rec == http.ErrAbortHandler {
panic(rec) // net/http's own signal — let it through
}
// ...log and respond 500
}
http.ErrAbortHandler is not a bug. It is net/http's way for a handler to
say "abandon this response silently", and the server watches for it. It
recovers like any other panic value, which is exactly the trap: swallow it
and you convert a deliberate, working abort into a spurious 500 and a
stack trace in your logs. Re-panic so the server sees the signal it is
expecting.
Practice
1. Recovering from Panics
IntermediateA handler that panics — a nil map write, an out-of-range index — would take down the whole server if nothing caught it. Recovery middleware turns a panic into a clean
500so one bad request doesn't kill the rest.Implement
Recover(next http.Handler) http.Handler: return a handler thatdefers a function which callsrecover(). Ifrecover()returns non-nil (a panic happened), respond500 Internal Server Error. Otherwise letnextrun normally.The
defermust be set up before you callnext.ServeHTTP— that's what puts it on the stack to catch the unwinding panic.package main import "net/http" // Recover wraps next so a panic inside a handler becomes a 500 response // instead of crashing the process. func Recover(next http.Handler) http.Handler { // TODO: return a handler that defers a function which calls recover(); // if recover() returns non-nil, respond 500 Internal Server Error. // Otherwise let next run normally. Set up the defer before calling // next.ServeHTTP. return next } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.