Skip to content
Muhammet Şafak
tr
Asked by: Oğuz Answered:

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.

  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

Tags: #resilience#go
Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

More Questions

All questions

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind