# 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://muhammetsafak.com/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: `net/http` already recovers a panic in a handler, so one bad request does not kill the process. But no proper response reaches the client: the connection is dropped and the stack trace goes only to the server's error log, which is stderr by default. A recovery middleware is what gives you a controlled `500`, structured logs with a request id and a metric — and it must re-panic `http.ErrAbortHandler`.

## Short answer

The real issue is this: the server's own recover is a safety net, not error handling. A nil deref in a handler doesn't take thousands of healthy requests down with it, but the client whose request panicked sees a broken connection instead of an answer, and the only trace is a raw stack on stderr. If your whole process did die, look for a panic outside that safety net: a goroutine started with `go`, where a panic nobody recovers terminates the program. I cover the details of building the service with the standard library in [writing HTTP services in Go](/blog/writing-http-services-in-go-is-the-standard-library-enough/).

## Why

1. **The built-in recover gives you no response and no signal.** For a panicking handler the server recovers, logs a stack trace to its error log, and either closes the network connection or sends an HTTP/2 `RST_STREAM`. The client gets no `500`, the log line has no request id, and nothing increments a metric.

2. **`recover` only catches its own goroutine.** The server's recover covers the goroutine that runs the handler. If you spawn `go func(){...}()` inside a handler, a panic there reaches neither the server's recover nor your middleware's — and it terminates the whole process, which is the case that drops every in-flight request.

3. **A silently swallowed panic is as dangerous as an unrecovered one.** Without a metric, the same bug stays hidden for days.

## What to do

1. **Wrap each request in `defer recover()`.** Inside the middleware set up `defer func(){ if r := recover(); r != nil { /* log + write 500 */ } }()`.

2. **Put this middleware at the outermost edge of the chain.** Recovery must wrap the panics of every handler and middleware that comes after it.

3. **Log the stack + request id, but don't leak internals.** Log the stack trace with `debug.Stack()` and the request's id; return a generic `500` to the client.

4. **Re-panic `http.ErrAbortHandler`.** If the recovered value is `http.ErrAbortHandler`, panic with it again instead of writing a `500`. It is the sentinel handlers use to abort a response on purpose, and re-panicking keeps the server's behavior for it: the client sees an interrupted response and no stack trace is logged.

5. **Make the panic visible — emit a metric.** Increment a counter on every recover.

6. **Give every goroutine you spawn its own recover.** Otherwise the process still dies.

**Bottom line:** I'd set up a per-request recover middleware that returns a controlled `500`, logs with the request id, counts the panic and re-panics `http.ErrAbortHandler` — plus a per-goroutine recover. 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

- [Writing HTTP services in Go: is the standard library enough?](/blog/writing-http-services-in-go-is-the-standard-library-enough/) — Blog
- [Does my channel buffer size choice hide backpressure problems, and how do I decide it?](https://muhammetsafak.com/just-ask/channel-buffer-size-choice-hide-backpressure-problems-decide/) — Just Ask
- [My Go worker in a container never receives SIGTERM and won't shut down gracefully—is this a PID 1 problem?](https://muhammetsafak.com/just-ask/go-worker-never-receives-sigterm-pid-1-problem/) — Just Ask
- [Should I run several dependent operations with errgroup so the first error cancels the rest?](https://muhammetsafak.com/just-ask/should-i-run-several-dependent-operations-with-errgroup-so-the-first/) — Just Ask
