# How do I handle panics in a Go HTTP server with recovery middleware?

> Wrap requests in a `defer recover()` middleware at the outermost layer, log the stack with the request id, return a generic 500 and emit a metric.

- Asked: 2026-06-01
- Answered: 2026-06-04
- Asked by: Oğuz
- Tags: dayaniklilik, go
- Source: https://www.muhammetsafak.com.tr/en/just-ask/panic-recovery-middleware-in-go-http-servers/
- Language: en-US
- Author: Muhammet Şafak

---
**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?


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.

1. **Wrap each request in `defer recover()`.** Inside the middleware set up `defer 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.
2. **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.
3. **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 generic `500 Internal Server Error`. Don't show the error detail, file paths or stack to the user.
4. **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.
5. **`recover` only catches the same goroutine — don't forget this.** If you spawn `go 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

- [Writing HTTP services in Go: is the standard library enough?](/en/blog/writing-http-services-in-go-is-the-standard-library-enough/) — Blog
- [Should I run several dependent operations with errgroup so the first error cancels the rest?](https://www.muhammetsafak.com.tr/en/just-ask/should-i-run-several-dependent-operations-with-errgroup-so-the-first/) — Just Ask
- [How do I propagate context cancellation correctly through nested service calls?](https://www.muhammetsafak.com.tr/en/just-ask/how-do-i-propagate-context-cancellation-correctly-through-nested-service-calls/) — Just Ask
- [How do I detect and prevent goroutine leaks in a long-running Go service?](https://www.muhammetsafak.com.tr/en/just-ask/how-do-i-detect-and-prevent-goroutine-leaks-in-a-long/) — Just Ask
