skip to content

What happens to a Go program when a goroutine panics and nothing recovers it?

level: juniorimportance: must knowfreq 75%

answer

  1. goroutines are not sandboxes
  2. blast radius is the whole binary
  3. other goroutines' defers never run
  4. runtime prints, then exits
  5. exit status 2

basics

~20 s

The whole process dies, not just that goroutine. The runtime prints the panic value and a stack trace and exits with status 2. Other goroutines are killed where they stand and their deferred functions never run.

solid answer

~50 s

A panic unwinds only the panicking goroutine's stack, running its deferred functions as it goes. If that unwinding reaches the top of the goroutine without any deferred call recovering, the runtime prints `panic: <value>` plus a stack trace and terminates the entire process with exit status 2. Go has no per-goroutine failure isolation: a goroutine started three layers deep in a library can take down a server that was happily handling thousands of requests. Nothing is returned to whoever executed the `go` statement, no goroutine is restarted, and deferred functions on the other goroutines do not get to run. Operationally this shows up as a container restart with one panic in the crash log and everything in flight lost. The only place a recover can prevent it is inside a function deferred by that same goroutine.

code

go · 7 lines
go
func main() {
	go func() {
		panic("boom")
	}()
	time.Sleep(time.Second)
	fmt.Println("still here") // never printed; process exits with status 2
}

go deeper

for a junior

Be ready to say plainly that an unrecovered panic anywhere ends the entire program, not just the goroutine, and that the runtime prints a stack trace and exits with status 2.

for a middle

Explain the mechanics: the panic unwinds one goroutine's stack running its defers, and going off the top of that stack is what makes it fatal. Note that other goroutines' deferred functions never run.

for a senior

Show you can work the incident backwards: a container restart plus one panic block at the end of the crash log, the created-by line naming the goroutine's origin, and GOTRACEBACK=all when one stack is not enough.

for a principal

Be ready to argue why fail-fast is a sane default for a shared-memory language, and what your service loses on every crash — in-flight work, cold caches, restart storms — when you weigh that default against containment.

## What `panic` does inside one goroutine Calling `panic(v)` stops the normal execution of the current function. The runtime then walks back up **that goroutine's** stack, running each deferred function it finds, frame by frame. If one of those deferred functions calls `recover()`, the panic stops there, `recover` returns `v`, and the goroutine resumes normally from the point where the deferred function's own frame returns. That is the recoverable case. The question here is what happens when nobody recovers. ## When nothing recovers: the process, not the goroutine If the unwinding reaches the bottom of the goroutine's stack with no `recover`, the panic becomes fatal to the **whole program**. The runtime prints something like: ``` panic: boom goroutine 7 [running]: main.dispatch(...) /app/dispatch.go:41 +0x25 created by main.run in goroutine 1 /app/main.go:18 +0x64 exit status 2 ``` and the process exits with **status 2**. This is the single most important fact about panics and goroutines: **there is no per-goroutine sandbox.** A goroutine is not a supervised worker, not an Erlang-style process, and not a thread whose crash a runtime absorbs. Its failure is the program's failure. Several consequences follow directly, and each one is a common surprise: - **Other goroutines are killed mid-work.** They are not asked to stop, they are not given a cancellation signal, and their **deferred functions do not run**. Buffers are not flushed, temporary files are not cleaned up, in-flight requests are dropped without a response. - **Nothing is returned to the starter.** `go f()` is not a call whose result you can inspect; the panic is not delivered to whoever wrote the `go` statement. - **The runtime does not restart anything.** There is no supervision built into Go. If you want a failed unit of work restarted, you write that yourself. - **`main` does not get a chance to react.** No `defer` in `main`, no `if err != nil`, no shutdown hook. ## Why the language is designed this way A panic means an assumption the code depended on has already been violated: an index was out of range, a nil pointer was dereferenced, an invariant a library checked has failed. At that moment the runtime knows nothing about what shared state the goroutine was half-way through changing. It may hold a mutex. It may have written two of three fields of a shared struct. Continuing to run the rest of the program means continuing on top of that damage. Crashing is therefore the conservative default: the operator gets a loud, dated, stack-carrying signal, and a restart puts the process back into a state that is known to be good. The alternative — silently isolating the failure — would require ownership boundaries between goroutines that Go deliberately does not have, since goroutines share one address space and communicate through ordinary memory and channels. ## Reading the crash in production In a deployed service, this failure has a recognisable shape: the platform records a **restart**, and the log for the terminated instance ends with exactly one panic block. Correlating the restart timestamp with the last lines of the crash log is usually the fastest way to identify the goroutine that died, because the traceback names the panicking function *and* the line that started the goroutine (`created by …`). How much of the traceback you get is controlled by the `GOTRACEBACK` environment variable. The default prints the stack of the **panicking goroutine** only. `GOTRACEBACK=all` prints every goroutine's stack, which is what you want when the panic is a symptom of something the rest of the program was doing. `GOTRACEBACK=crash` additionally makes the process abort so the operating system can write a core dump. ## What this does *not* mean It does not mean panics are unmanageable — it means the management has to be inside the goroutine that can panic. A deferred function placed in the body that goroutine runs, calling `recover`, is the only thing that can intercept it. A `recover` in the parent, in `main`, or in a shared helper on another goroutine cannot. It also does not mean every goroutine should be wrapped defensively. Recovering a panic you do not understand keeps a process alive on top of state you cannot vouch for, which is why crashing is a legitimate and often preferable posture — and why the decision belongs to whoever owns the service's availability rather than to the author of one goroutine. ## The practical rule Assume any panic anywhere is fatal to the whole binary. If a goroutine runs code you do not fully control — a callback registered by another package, a handler supplied by another team — treat containing its panics as a deliberate design step, and expect to write that containment inside the goroutine's own body.

  • Do deferred functions on the other goroutines run when an unrecovered panic ends the program?
    No. Only the panicking goroutine's own defers run, as part of the unwinding. Once the runtime decides the panic is fatal it terminates the process directly, so cleanup deferred elsewhere — flushing a buffered writer, removing a temp file, closing a file handle — is simply skipped. Anything that must survive a crash has to be durable already, not deferred.
  • How do you get the stacks of every goroutine, not just the panicking one, out of a crash?
    Set `GOTRACEBACK=all` in the process environment. The default traceback prints only the goroutine that panicked, which hides the context when the real cause is what another goroutine was doing at the time. `GOTRACEBACK=crash` goes further and aborts the process so the OS can write a core dump for offline inspection.
  • Why did Go's designers make a panic fatal to the whole process rather than to just that goroutine?
    Because a panic means an invariant is already broken and the runtime cannot know what shared state the goroutine left half-modified — a held mutex, a partially written struct. Goroutines share one address space with no ownership boundary, so isolating the failure would mean continuing on top of unknown damage. Crashing gives a loud signal and a restart into a known-good state.

A panic is the building's fire alarm, not the alarm on one room's door: every goroutine leaves the building, including the ones doing perfectly healthy work.

saying these in an interview costs you the question

  • Says only the panicking goroutine dies and the service keeps running
  • Expects the runtime to restart the failed goroutine automatically
  • Thinks the panic is returned as an error to whoever wrote go
  • Believes a recover in main protects goroutines main started
  • Assumes deferred cleanup on other goroutines still runs