skip to content

What happens in Go when a deferred function panics while the goroutine is already unwinding from an earlier panic?

level: middleimportance: nice to knowfreq 28%

answer

  1. the walk up the stack does not restart
  2. later deferred calls are not abandoned
  3. the runtime keeps a chain, not one value
  4. the report prints the oldest one first
  5. the newest value is what an inspector sees

basics

~20 s

Unwinding continues with the new panic, and the remaining deferred calls further up the stack still run. The runtime remembers the chain and prints every panic, the original first with each later one indented beneath it, before exiting with status 2.

solid answer

~50 s

Panicking during a panic is legal and does not abort the walk. The new panic replaces the active one as the value in flight, but the runtime keeps the earlier one, and unwinding carries on: deferred calls in the current frame that have not run yet are skipped only from the panicking deferred call onward, and every frame above still runs its deferred calls. If nothing intercepts anything, the crash report lists the whole chain — `panic: first` on one line, then `\tpanic: second` indented beneath — so the original cause is still visible in the output. The practical hazard is that a deferred cleanup function that can itself panic replaces the value that any interceptor would see with its own, so a diagnostic wrapper that only inspects the latest value reports the cleanup failure and loses the real cause. Keep deferred cleanup simple and non-panicking.

code

go · 5 lines
go
func f() {
	defer fmt.Println("cleanup")
	defer panic("second")
	panic("first")
}

go deeper

for a junior

Know that panicking inside a deferred function is allowed and that the program still ends with one crash report. You are not usually expected to predict the exact output at this level, but you should not think it is forbidden.

for a middle

Explain the chain: the new panic becomes the active value, unwinding continues, the remaining deferred calls up the stack still run, and the report prints the oldest panic first with later ones indented.

for a senior

Point at the operational trap — a panicking cleanup replaces the value any wrapper inspects, so logs can name the symptom while the crash report still holds the cause — and state the rule that deferred cleanup must be total and non-panicking.

for a principal

Turn this into a review standard: deferred bodies stay trivial, cleanup that can fail reports through a named result, and crash reports are captured whole so a multi-panic chain is never truncated to its last line in your logging pipeline.

## The rule A panic raised while the goroutine is already unwinding does not restart, abort or double the process of unwinding. The runtime maintains a linked chain of active panics on the goroutine. The new one becomes the *current* panic, the older one is retained beneath it, and the walk up the stack resumes from where it was. Concretely, in ```go func f() { defer fmt.Println("cleanup") defer panic("second") panic("first") } ``` `panic("first")` starts unwinding. Deferred calls run last-registered-first, so `panic("second")` runs next and raises a second panic. Unwinding continues with the remaining deferred calls of the same frame, so `cleanup` still prints, and then the walk moves to `f`'s caller and runs its deferred calls too. ## What the report looks like With nothing intercepting, the runtime prints the whole chain, oldest first, with later panics indented: ``` panic: first panic: second goroutine 1 [running]: ... ``` This matters at 3am: **the original cause is not lost from the output.** A crash whose report has more than one `panic:` line is telling you that a cleanup path failed while an earlier failure was already unwinding, and the top line is the one that started it. The process still exits once, with status 2. There is no notion of "two crashes". ## Where the danger actually is The output keeps the chain, but the *value* in flight is the newest panic. Any code that later inspects the panic value — a wrapper that logs it, a test helper, a per-request guard — sees the panic raised by the cleanup, not the one that caused the cleanup to run. If your deferred `Close()` panics because the connection was already torn down by the original failure, your logs can end up naming the symptom and hiding the cause even though the crash report would have shown both. The defence is a discipline about deferred code rather than clever handling: - Deferred cleanup should be **total**: it must work regardless of how far the function got. A `defer` registered before the resource exists, or one that assumes a field was populated, is a panic waiting to happen on the failure path. - Prefer `defer f.Close()` over deferred logic with branches, indexing or type assertions in it. - If a deferred function genuinely can fail, have it record the failure — assign to a named result, or log it — rather than panic. ## Related but distinct: re-panicking on purpose Deliberately raising a new panic after intercepting one is a different, legitimate pattern used to add context; it is worth keeping separate in your head from *accidentally* panicking inside cleanup. The mechanism is the same chain; the difference is intent and whether you preserve the original value in the new one. ## What an interviewer is checking This question separates candidates who have only read that "defers run on panic" from those who have thought about what happens when the panic path itself is buggy. The three facts to land are: unwinding continues, the remaining deferred calls still run, and the crash report shows the whole chain with the original at the top.

  • If a second panic is raised during unwinding, is the first one lost?
    Not from the crash report — the runtime keeps the chain and prints `panic: first` with the later one indented beneath it, so the original cause is visible. It is lost from the *value* in flight: anything that inspects the active panic afterwards sees only the newest one, which is why a panicking cleanup function can make logs name the wrong failure.
  • How should a deferred cleanup function that can genuinely fail be written?
    Have it report rather than panic. Assign the failure to a named result parameter so the caller receives it as an ordinary error, or log it with enough context to correlate. Keep the deferred body free of indexing, type assertions and dereferences of state the function may not have initialised yet, because deferred code runs on the failure path where exactly those things are unset.

saying these in an interview costs you the question

  • Says the second panic aborts unwinding immediately
  • Claims remaining deferred calls are skipped
  • Thinks the runtime prints only the newest panic
  • Believes panicking twice is a fatal runtime error
  • Assumes the process exits twice or with a different status