q.Recover and q.RecoverE¶
Function-wide panic-to-error conversion. defer q.Recover(&err) at the top of a function catches any panic, wraps it in *q.PanicError, and assigns it to the named error return. Pure runtime — no preprocessor rewriting.
Signatures¶
func Recover(errPtr *error)
func RecoverE(errPtr *error) RecoverResult
type PanicError struct {
Value any
Stack []byte
}
The whole family is plain runtime code — Go's recover() sees the panic because q.Recover (or RecoverE's terminal method) IS the deferred function.
What q.Recover does¶
Two equivalent forms — the zero-arg auto form is rewritten by the preprocessor into the explicit form:
// Auto (preprocessor rewrites): err-return auto-named to `_qErr`
// and the defer call auto-wired to `&_qErr`.
func doWork(input Input) error {
defer q.Recover()
process(input)
return nil
}
// Explicit (pure runtime — works without the preprocessor too).
func doWork(input Input) (err error) {
defer q.Recover(&err)
process(input)
return nil
}
At runtime (either form):
- If
processreturns normally →err == nil, function returnsnil. - If
processpanics with valuer→q.Recovercatches it, assigns&q.PanicError{Value: r, Stack: debug.Stack()}toerr, function returns that error.
Auto-form rules:
- The enclosing function must have the built-in
erroras its last return. Concrete error types (MyErr,*MyErr, …) are rejected — a&errof any type other than*errorwould be a type mismatch againstq.Recover's signature. - If the error return is already named, the preprocessor reuses the name (
errin the example above). - If unnamed, the preprocessor injects a name (
_qErr). Go requires all-or-nothing naming of results, so sibling unnamed slots are also named — as_qRet0,_qRet1, etc. The rewritten signature is internal; callers see the same types as before.
Callers can unwrap the panic:
var pe *q.PanicError
if errors.As(err, &pe) {
log.Printf("panic value: %v", pe.Value)
log.Printf("stack:\n%s", pe.Stack)
}
Chain methods on q.RecoverE¶
Each method is terminal — it's the deferred function. The recover() inside each method is what catches the panic.
| Method | Stored in *errPtr on panic |
|---|---|
.Map(fn func(any) error) |
fn(panicValue) — full custom translation |
.Err(replacement error) |
replacement — discard panic value and stack |
.ErrF(fn func(*PanicError) error) |
fn(&PanicError{…}) — see the wrapper, return a richer error |
.Wrap(msg string) |
fmt.Errorf("<msg>: %w", &PanicError{…}) |
.Wrapf(format string, args ...any) |
fmt.Errorf("<format>: %w", args..., &PanicError{…}) |
defer q.RecoverE(&err).Map(func(r any) error {
if s, ok := r.(BusinessRuleViolation); ok {
return &APIError{Code: 400, Detail: s.String()}
}
return &APIError{Code: 500, Detail: fmt.Sprint(r)}
})
The zero-arg auto form also works for q.RecoverE:
func doWork() error {
defer q.RecoverE().Map(func(r any) error { return &APIError{Detail: fmt.Sprint(r)} })
...
}
Runtime-only, deliberately¶
The "chain method IS the deferred function" property is why this works without preprocessor rewriting. Don't refactor it into helper calls — recover() only sees panics when called directly from a deferred function, not transitively.
For goroutine-local recovery, write the defer func() { if r := recover(); r != nil { … } }() block yourself — q deliberately doesn't ship an opinion about what to do with goroutine panics (log? report to Sentry? crash the process?). Your call.
See also¶
- q.Try — the explicit error-forwarding counterpart.