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.
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.
- Wrap each request in
defer recover(). Inside the middleware set updefer func(){ if r := recover(); r != nil { /* log + write 500 */ } }(). That way a panic in a single request is caught, the process stays up, and other requests are unaffected. - Put this middleware at the outermost edge of the chain. Recovery must be the outermost layer so it wraps the panics of every handler and middleware that comes after it. If it sits inside, you’ll miss a panic from something further out.
- Log the stack + request id, but don’t leak internals. Log the stack trace with
debug.Stack()and the request’s id; to the client return a generic500 Internal Server Error. Don’t show the error detail, file paths or stack to the user. - Make the panic visible — emit a metric. Increment a counter/metric on every recover. A silently swallowed panic is as dangerous as an unrecovered one; if you recover but forget the metric, the same bug stays hidden for days.
recoveronly catches the same goroutine — don’t forget this. If you spawngo func(){...}()inside a handler, a panic in that goroutine won’t hit your middleware’s recover and will still kill the whole process. Every goroutine you spawn needs its own recover.
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. I cover the details of writing an HTTP service with Go’s standard library in the hub post.
Related Reading
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.