What does a Go panic do to the goroutine's stack, and how does the program end if nothing recovers it?
answer
- normal execution stops, cleanup does not
- deferred calls still get their turn
- reverse order, frame by frame, one goroutine
- the process dies, not just the goroutine
- check the shell's exit status
basics
~20 sA panic stops normal execution and unwinds the goroutine's stack, running every deferred call on the way up. Unrecovered, the runtime prints the panic value plus a stack trace to standard error and exits the whole process with status 2.
solid answer
~50 sA panic — either an explicit `panic(v)` call or a runtime failure such as a nil pointer dereference — stops the current function at that point. The rest of that function's body is skipped, but its deferred calls run, last-registered first; then the frame is popped and the caller's deferred calls run, and so on all the way up that one goroutine's stack. Nothing else in the program is unwound: other goroutines keep running until the panic reaches the top of the panicking goroutine. At that point the runtime prints `panic: ` followed by the panic value and a stack trace to standard error, and terminates the entire process with exit status 2 — not just the goroutine. So a panic is a whole-program failure by default, which is why Go reserves it for programmer bugs and returns `error` values for expected failures.
code
go · 10 linesfunc middle() {
defer fmt.Println("middle defer")
inner()
fmt.Println("never printed")
}
func inner() {
defer fmt.Println("inner defer")
panic("boom")
}go deeper
Be ready to say, in one breath, that a panic unwinds the current goroutine's stack running deferred calls in reverse order, and that an unrecovered one crashes the whole program with exit status 2. Practise predicting the output order of a small program.
Explain the mechanics: which statements are skipped, why deferred calls still run, that unwinding is per goroutine, and why the panic message appears after deferred output. Contrast it with os.Exit, which runs no deferred calls at all.
Show the operational consequence: any goroutine's panic kills the service, standard error must be captured, and exit status 2 is what your supervisor or orchestrator sees. Be able to argue when returning an error beats panicking in code others call.
Own the policy question of where panics are allowed at all in your codebase — typically only at startup for unusable configuration — and what a crash costs you in in-flight work, given that other goroutines lose their deferred cleanup when the process exits.
## What a panic is A *panic* is Go's mechanism for an unrecoverable programming error. It comes from two places: 1. **An explicit call**: `panic(v)`, where `v` is any value (`panic` takes an `any`). 2. **The runtime**: dereferencing a nil pointer, indexing a slice out of range, dividing an integer by zero, a failed single-value type assertion, writing to a nil map, sending on a closed channel, and similar memory- or type-safety violations. These are indistinguishable from an explicit panic once they start; the runtime simply supplies the value. ## Unwinding, step by step When a panic starts, the panicking function stops immediately at the panic point. **Its remaining statements never run.** What does run is its deferred calls, in last-in-first-out order — the same order they would run on a normal return. When that frame's deferred calls are exhausted, the frame is discarded and the runtime moves to the caller, running *its* deferred calls, and repeats up the stack. So unwinding is not "skip everything and die": it is an orderly walk up one goroutine's stack in which every `defer` gets its turn. That is exactly why `defer f.Close()` and `defer mu.Unlock()` are safe idioms — they hold even on the panic path, which an `if err != nil { return }` chain of manual cleanup would not. Two boundaries matter: - **Unwinding is per goroutine.** Only the panicking goroutine's frames are walked. Deferred calls registered in other goroutines are never reached by this panic. - **Unwinding stops at the top of that goroutine.** There is no outer frame to hand the panic to, so the runtime takes over. ## What the runtime prints, and how the process ends If no deferred function stops the panic, the runtime writes to standard error: ``` panic: boom goroutine 1 [running]: main.inner() /app/main.go:14 +0x27 main.main() /app/main.go:5 +0x17 ``` The first line is `panic: ` plus the value. If the value implements `error` the runtime prints its `Error()` result; if it implements `fmt.Stringer` it prints `String()`; otherwise it prints the value in a default form. For a runtime panic the text begins `runtime error: `, as in `runtime error: index out of range [5] with length 3`. Then the runtime **exits the whole process with status 2**. This is the fact people get wrong most often: a panic in any goroutine — a background worker, a timer callback, a request handler's helper — takes the program down, not just that goroutine. Other goroutines are stopped wherever they happen to be, so their deferred calls never run and their in-flight work is lost. Exit status 2 is what a supervisor, a shell (`echo $?`), or a container runtime sees. It distinguishes a Go crash from a clean `os.Exit(1)` your own code chose. ## Ordering: defers first, message second Because the message is printed only after unwinding finishes, output written by deferred calls appears **before** the `panic:` line. A program that prints "closing file" from a `defer` will show that line first, then the panic. Candidates often predict the opposite. ## What a panic is not - **Not an exception you throw for control flow.** Go's error handling is values: functions return `error` and callers check it. Panics are for "this cannot happen" conditions and for genuinely unusable state at startup. - **Not `os.Exit`.** `os.Exit(n)` terminates immediately with status `n` and runs **no** deferred calls at all. A panic runs them, then exits with 2. - **Not a per-goroutine failure.** Isolation must be built deliberately; it is not the default. ## Practical consequences Because a panic ends the process, panicking in library code you publish is a strong statement: every caller inherits the crash. Prefer returning an `error` unless the caller's program is definitely broken (an impossible switch branch, an invalid compile-time-constant configuration). Because defers *do* run during unwinding, they are the right place for resource cleanup — but keep them cheap and non-panicking, because a deferred call that panics while a panic is already in flight complicates the report. And because the process exits with status 2 and dumps to standard error, make sure your deployment actually captures standard error. A crash whose output goes nowhere is the hardest kind to debug at 3am.
- Do deferred calls in other goroutines run when one goroutine's panic takes the process down?No. Unwinding walks only the panicking goroutine's frames. When it reaches the top, the runtime exits the process, and every other goroutine is stopped wherever it happens to be — its deferred calls never run. So `defer` is not a mechanism for flushing buffers or releasing resources held by other goroutines during a crash.
- How does `os.Exit` differ from an unrecovered panic?`os.Exit(n)` terminates the process immediately with status `n` and runs no deferred calls whatsoever — no unwinding, no stack trace. An unrecovered panic first runs the panicking goroutine's deferred calls, then prints the panic value and stack trace to standard error, then exits with status 2. If you need cleanup, never reach for `os.Exit` in the middle of a call chain.
- What kinds of value can you pass to `panic`?Any value — `panic` takes an `any`. Idiomatic code passes an `error` or a short string. When printing, the runtime calls `Error()` if the value implements `error`, `String()` if it implements `fmt.Stringer`, and otherwise prints a default representation. Passing an error value is preferred because code that intercepts the panic can then type-assert it meaningfully.
saying these in an interview costs you the question
- Thinks a panic ends only the goroutine, not the process
- Says deferred calls are skipped once a panic starts
- Expects deferred calls to run after os.Exit
- Assumes the process exits with status 1
- Believes the panic message prints before deferred output
- Treats panic as ordinary exception-style control flow