skip to content

What happens when an http.Handler in a Go net/http server panics?

level: juniorimportance: must knowfreq 62%

answer

  1. the process is still running afterwards
  2. one connection pays, not the server
  3. the client never receives a status line
  4. the stack trace lands on Server.ErrorLog

basics

~20 s

Go's net/http server recovers a panic raised by a handler, so the process keeps serving other requests. It prints the panic value and a stack trace to Server.ErrorLog and closes that one connection, so the client gets no HTTP status at all.

solid answer

~50 s

`net/http` runs every connection under a deferred recover, so a panicking handler does not take the process down: only that one connection dies. The server writes `http: panic serving <remote addr>: <panic value>` plus the stack to `Server.ErrorLog` (or to the standard `log` package, which means stderr, when `ErrorLog` is nil), then closes the connection without writing any response. From the client's side there is no 500 and no status line at all — it sees a closed or reset connection, or a truncated body if the handler had already written and flushed part of the response. Requests in flight on other connections are untouched. That behaviour is damage control, not error handling: because the client never receives a real status and the log line carries no request context, most services add a recovery middleware that recovers inside the handler's own goroutine and turns the panic into a proper 500.

code

text · 4 lines
text
2026/09/02 11:04:19 http: panic serving 10.1.2.3:53112: assign to entry in nil map
goroutine 34 [running]:
main.(*catalog).put(...)
main.updateHandler(...)

go deeper

for a junior

Be ready to say plainly that the process survives, that only that connection is dropped, and that the client does not get a 500. Knowing where the stack trace is printed is a bonus.

for a middle

An interviewer expects the mechanics: a deferred recover per connection, the log line on Server.ErrorLog with the standard logger as fallback, no response written, connection closed.

for a senior

Show that you know the failure is invisible as a server error — clients report transport failures — and that the surviving process may hold broken invariants such as a mutex never unlocked.

for a principal

Own the framing that this default is a process-level safety net rather than a service policy, and that choosing between silent connection drops and explicit 500s is a decision your team makes on purpose.

## The short version A panic in an `http.Handler` does **not** crash a Go HTTP server. `net/http` guards each accepted connection with its own deferred `recover`, logs what happened, and drops that connection. The process, and every other connection it is serving, carries on. ## What a panic is, in one paragraph A *panic* is Go's abrupt failure path: the runtime stops normal execution of the current goroutine, runs its deferred functions in reverse order as it unwinds the stack, and — if nothing intervenes — prints a stack trace and kills the process. `recover()`, called from inside a deferred function, stops that unwinding and returns the value the panic carried. Typical accidental panics in a handler are a nil map write, an index out of range, a nil pointer dereference, or a failed type assertion. ## What the server actually does When `http.Server` accepts a connection it starts a goroutine to serve it, and that goroutine's first deferred function contains a `recover()`. If a handler panics, that deferred function catches it and: 1. **Logs it.** The line has the shape `http: panic serving 10.1.2.3:53112: assign to entry in nil map`, followed by the stack trace of the panicking goroutine. It goes to `Server.ErrorLog`, a `*log.Logger` field you can set. If you leave it nil, the server falls back to the standard `log` package, whose default destination is standard error. 2. **Closes the connection.** It does not write a status line, a header, or a body. It does not retry the request. 3. **Returns normally.** The panic stops there; the process is not terminated and other connections never notice. ## What the client sees This is the part that surprises people. There is **no automatic 500**. Depending on how far the handler got: - **Nothing written yet** — the client's HTTP library reports a broken/reset connection or an unexpected EOF while reading the response. Many clients surface that as a transport error, not as an HTTP status, which means your dashboards may show it as a client-side failure rather than a server error. - **Headers already sent** — the client has a 200 (or whatever was written) and then the body simply stops. On a chunked response the terminating chunk never arrives, so a strict client reports a truncated body; a sloppy one may accept a half-decoded JSON document. Neither is a good production experience, which is exactly why recovery middleware exists. ## What this recovery does *not* cover - **Other goroutines.** `recover()` only works for the goroutine that panicked. If your handler starts `go doSomething()` and *that* goroutine panics, no deferred recover is on its stack, the runtime prints a traceback to standard error and the whole process exits. - **Runtime fatal errors.** Some failures are *throws*, not panics, and are deliberately unrecoverable: `fatal error: concurrent map writes`, `fatal error: all goroutines are asleep - deadlock!`, and out-of-memory. No `recover` anywhere catches these; the process dies. - **Invariants.** Recovering means the request is abandoned midway. A mutex locked without `defer mu.Unlock()` stays locked, a half-updated cache stays half-updated. The server survives; your data structures may not. ## Why you still write a recovery middleware The built-in behaviour is a safety net for the *process*, not a policy for your *service*. A recovery middleware recovers earlier — inside the goroutine running `ServeHTTP`, before the connection layer sees the panic — so it can write a real status code, log with the method, path and request id attached, increment a metric, and let the handler return normally, which leaves the connection healthy and reusable instead of closed. ## Things to say in an interview - The process survives; one connection does not. - The client gets no status, not a 500. - The stack trace goes to `Server.ErrorLog`, defaulting to stderr via the standard logger. - Fatal runtime errors and panics in other goroutines are outside this net entirely.

  • If the handler had already written a 200 and part of the body before panicking, what does the client get?
    The 200 is already on the wire and cannot be taken back, so the client sees a successful status with a body that just stops. On a chunked response the final chunk never arrives, so a strict client reports a truncated or unexpected-EOF read. This is why recovering *before* anything is written matters more than recovering perfectly.
  • Are all runtime failures caught by that per-connection recovery?
    No. Some failures are runtime throws rather than panics and are deliberately unrecoverable: `fatal error: concurrent map writes`, the all-goroutines-asleep deadlock detector, and out-of-memory. Those kill the process regardless of any `recover`. A panic in a goroutine your handler started is also outside the net, because recover only works within the goroutine that panicked.
  • Where do those panic log lines go if you never set Server.ErrorLog?
    To the standard `log` package's default logger, which writes to standard error. In a container that means the panic ends up in the raw stdout/stderr stream rather than in your structured application logs, which is why it is common to set `Server.ErrorLog` to a logger you control.

It is a circuit breaker on one socket, not a fuse for the whole building: that outlet goes dead and everything else keeps running, but nobody plugged in there gets a polite explanation.

saying these in an interview costs you the question

  • Claims a handler panic crashes the whole Go server process
  • Says net/http automatically sends the client a 500
  • Thinks the server retries the request after a handler panic
  • Believes a recover elsewhere in the program catches it
  • Assumes every runtime failure, including concurrent map writes, is recoverable