How do you stop a panic from escaping a Go function and return it to the caller as an error?
answer
- the caller should never see a panic
- only a deferred call can intervene
- an anonymous result cannot be reassigned
- set err inside the deferred closure
- register it before the risky work
basics
~20 sGive the function a named error result and defer a closure that calls recover. When recover returns non-nil, the closure assigns a wrapped error to that named result, so the caller sees an ordinary error instead of a panic.
solid answer
~40 sThe conversion has three parts. First, name the error result — `func Reconcile(k Key) (err error)` — because only a named result can be reassigned once the function has stopped running normally. Second, `defer` the recovery closure at the very top, before anything that can fail; a call to `recover` only has an effect inside a function the panicking goroutine deferred. Third, inside that closure write `if r := recover(); r != nil { err = fmt.Errorf("reconcile %v: recovered panic: %v", k, r) }`. The function then returns normally carrying that error. I keep the boundary narrow — one per exported entry point or per goroutine top, never sprinkled through internal helpers — and the message always says the error came from a panic, so nobody mistakes a bug for an expected failure.
code
go · 10 linestype Key struct{ Namespace, Name string }
func Reconcile(k Key) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("reconcile %v: recovered panic: %v", k, r)
}
}()
return apply(k)
}go deeper
Be ready to write the pattern from memory: named error result, deferred closure at the top, recover checked for non-nil, error assigned. Know that calling recover straight in the function body does nothing.
Explain why the result must be named and why the closure must be deferred: the assignment happens after the body has stopped running, and only an already-registered deferred call executes while the stack unwinds.
Show where you put this boundary in a real service and how the error stays actionable — which unit of work failed, that it came from a panic, and enough detail for whoever reads it at 3am.
Own the policy: which boundaries in the codebase may convert panics, what those errors must contain, and why recovery scattered through helpers hides bugs rather than containing them.
## What the boundary is for A panic in Go is not an exception you catch at a convenient place. It unwinds the goroutine's stack, running deferred functions as it goes, and if nothing stops it the whole **process** exits with a traceback. A recovery boundary is the one place where you deliberately stop that unwinding and turn the failure into a value the surrounding code already knows how to handle: an `error`. ## The three ingredients ### 1. A named result parameter ```go func Reconcile(k Key) (err error) { ... } ``` `err` here is a real variable that lives for the whole call, not just a type in the signature. A deferred closure can read and write it. If the signature were `func Reconcile(k Key) error`, the result would be anonymous — the closure has nothing to name, so it cannot change what the caller receives. This is the single most common mistake when writing the pattern for the first time: the recovery runs, the panic is stopped, and the function returns `nil` because nobody could assign to the result. ### 2. A deferred closure, registered before the risky work ```go defer func() { if r := recover(); r != nil { err = fmt.Errorf("reconcile %v: recovered panic: %v", k, r) } }() ``` Only functions that were already deferred when the panic happened get to run during unwinding. A `defer` placed after the call that panics is never registered, so it never runs. Put the recovery closure at the top of the function body. A related subtlety: deferred calls run last-in-first-out. If the function also defers cleanup that sets `err` (closing a file, committing or rolling back), registering the recovery closure **first** means it runs **last**, so it sees and can overwrite whatever the cleanup put in `err`. That ordering is usually what you want at a boundary. ### 3. Checking the recovered value `recover()` returns the value passed to `panic`, typed as `any`. It is `nil` when the goroutine is not panicking, so the `if r := recover(); r != nil` guard is what distinguishes a normal return from a recovered one. The value can be anything — a string, an `error`, a custom type — so format it with `%v` rather than assuming it is an `error`, or type-switch on it if you care. ## What the resulting error should say A converted panic is a bug, not an expected outcome, and the error should read that way. `fmt.Errorf("reconcile %v: recovered panic: %v", k, r)` tells a reader three things: which unit of work failed, that the failure was a panic rather than a returned error, and what the panic value was. If callers need to branch on it, define a small error type that carries the recovered value (and, for a real service, the stack captured at the same moment) instead of flattening everything into a string. ## Where the boundary belongs One boundary per *unit of work that must not take the process down*: the top of each goroutine you start, an exported entry point of a library that must not panic into a caller's code, the per-item step of a loop that processes many independent items. Not inside every helper. Blanket recovery deep in the call tree converts programming bugs into vague errors far from where they happened and lets the program continue on state that a half-finished function left behind. Equally, never recover into silence. `defer func() { recover() }()` compiles, stops the panic, and returns the zero values — a function that reports success after a crash is worse than one that crashes. ## Worked shape ```go func Reconcile(k Key) (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("reconcile %v: recovered panic: %v", k, r) } }() return apply(k) // may panic deep inside } ``` If `apply` panics, the deferred closure runs while the stack unwinds, `recover` hands back the panic value, `err` is assigned, unwinding stops at this frame, and `Reconcile` returns to its caller like any other failing function. If `apply` returns an error normally, `recover` returns `nil`, the closure leaves `err` alone, and the caller gets exactly what `apply` produced. ## Summary Named result, defer first, check `recover()` for non-nil, assign a self-describing error. Place it where a failure must be contained, say in the message that it was a panic, and resist the urge to install one everywhere.
- Why does the recovery closure have to be deferred before the code that panics, not after it?Only defers that were already registered when the panic started run during unwinding. A `defer` statement placed after the failing call is never reached, so it never registers, and the panic continues past the frame. Registering at the top of the function body is the habit that makes the boundary reliable.
- If the function also defers cleanup that sets err, does the ordering between the two defers matter?Yes. Deferred calls run last-in-first-out, so the closure you defer first runs last. Registering the recovery closure first lets it observe and overwrite whatever the cleanup assigned to the named error, which is normally what you want: a recovered panic should not be masked by a benign close error.
- What is wrong with `defer func() { recover() }()` at the top of a function?It stops the panic and discards it. The function returns its zero values, so the caller is told the work succeeded when it actually crashed mid-way. A recovery boundary must always turn the panic into a reported failure — an assigned error, a status update, or at minimum a logged record — never into silence.
saying these in an interview costs you the question
- Calling recover in the function body instead of inside a deferred closure
- Using an anonymous error result and expecting the assignment to reach the caller
- Deferring the recovery closure after the code that panics
- Recovering and returning nil, reporting success after a crash
- Wrapping every internal helper in its own recovery boundary