How do I handle panics in a Go HTTP server with recovery middleware?
Question
In our HTTP API written in Go, an unexpected panic occurred due to a nil pointer dereference. The panic caused the whole process to shut down abruptly and dropped every HTTP request that was in flight at the time. How do I set up a central middleware that uses Go's `recover()` to catch panics, log them and return a graceful 500 to the client?
Answer
Short answer: an unrecovered panic in one handler crashes the whole process and drops every in-flight request. The fix is to wrap each request in a recovery middleware — so a panic in one request is contained to that request.
Short answer
The real issue is this: in Go, if a goroutine panics and nobody recovers, the program shuts down entirely. Because of one nil deref, thousands of healthy requests die with you. I cover the details of building the service with the standard library in writing HTTP services in Go.
Why
-
A panic is process-level, not request-level. Unrecovered, the blast radius covers the whole process, not one request.
-
recoveronly catches its own goroutine. If you spawngo func(){...}()inside a handler, a panic there won’t hit your middleware’s recover. -
A silently swallowed panic is as dangerous as an unrecovered one. Without a metric, the same bug stays hidden for days.
What to do
-
Wrap each request in
defer recover(). Inside the middleware set updefer func(){ if r := recover(); r != nil { /* log + write 500 */ } }(). -
Put this middleware at the outermost edge of the chain. Recovery must wrap the panics of every handler and middleware that comes after it.
-
Log the stack + request id, but don’t leak internals. Log the stack trace with
debug.Stack()and the request’s id; return a generic500to the client. -
Make the panic visible — emit a metric. Increment a counter on every recover.
-
Give every goroutine you spawn its own recover. Otherwise the process still dies.
Bottom line: I’d set up a per-request recover middleware + a per-goroutine recover + metrics. Two caveats: recover is for unexpected bugs (nil deref), not a substitute for returning errors — return expected errors as error. And keep a supervisor that restarts the process anyway.
Related Reading
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.