What happens with panic(nil) and with re-panicking? How did Go 1.21 change this?
Before Go 1.21, panic(nil) made recover() return nil, so code that tested if r := recover(); r != nil couldn't tell "no panic" from "panicked with nil" and swallowed the panic silently. Since Go 1.21, panic(nil) (or a nil interface) turns into a panic with *runtime.PanicNilError, so recover returns a non-nil value. GODEBUG=panicnil=1 restores the old behavior.
func main() {
defer func() {
r := recover()
fmt.Printf("%T: %v\n", r, r)
// *runtime.PanicNilError: panic called with nil argument
}()
panic(nil)
}
Re-panicking: a deferred function that recovers can call panic again, either to escalate unknown panics or to add context. The new panic replaces the old one, and the crash output shows both, with the first marked [recovered].
defer func() {
if r := recover(); r != nil {
if pe, ok := r.(parseError); ok {
err = pe.err // our own sentinel panic: convert to error
return
}
panic(r) // not ours: let it crash
}
}()
Go 1.25 made crash output clearer when a recovered panic is re-panicked with the same value: the value is printed once, marked [recovered, repanicked], instead of being repeated.
More on Error Handling & panics
- Q308What does this print? (Defer order during panic)
- Q309How do you convert a panic into an error returned from a function?
- Q311Which runtime failures can't be recovered with
recover? - Q312Compare
panic,os.Exit,log.Fatalandruntime.Goexitwith respect to deferred functions. - Q313How do you propagate errors from multiple goroutines? Explain
errgroup. - Q314How do you collect all errors from concurrent workers rather than just the first?