Skip to content

Day 020 — panic, recover & When to Use Them

Month 1 · Week 3 · ⬅ Day 019 · Day 021 ➡ · Journal index

🎯 Learning Objective

Understand how panic/recover interact with defer, and develop sound judgment about the narrow situations where they belong (and the many where they don't).

📚 Topics

  • panic unwinding the stack · running deferred funcs · recover only in a deferred func
  • Converting a panic into an error at a boundary
  • Re-panicking · recover() returns the panic value
  • Errors vs panics: ordinary failure vs programmer bug / unrecoverable state

📖 Reading / Sources

📝 Notes

  • panic stops normal flow and starts unwinding the stack, running each function's deferred calls on the way up → [[defer-lifo]]. If nothing recovers, the program crashes and prints the panic value + stack trace.
  • recover stops the unwinding — but only works when called directly inside a deferred function. Anywhere else it returns nil and does nothing.
  • recover() returns the value passed to panic (an any), or nil if there's no active panic. A common pattern: assign it to a named return to convert a panic into an error at a package boundary.
  • Idiomatic rule: use errors for expected failures (bad input, missing file, network) and panic only for programmer bugs / truly unrecoverable states (impossible switch case, nil that "can't" be nil, failed package-init invariant). Don't use panic/recover as exceptions for control flow → [[errors-are-values]].
  • The standard library does this: regexp.MustCompile panics (programmer gave a bad pattern at startup), while regexp.Compile returns an error (runtime input). Must… constructors panic by convention.
  • A library that does recover should convert to an error at its boundary so panics don't leak to callers; or re-panic (panic(r)) if the value isn't one it knows how to handle.
  • net/http's server already recovers per-request so one handler's panic won't kill the process — but you should still return errors, not rely on that.
  • Some panics cannot be recovered cleanly in the same goroutine flow you'd expect — e.g. a panic in another goroutine crashes the whole program unless that goroutine recovers. Each goroutine must guard itself → [[goroutines]].

💻 Code Examples

// safeDivide converts a panic into an error using a deferred recover
// assigned to a NAMED return value.
func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered: %v", r) // turn the panic into an error
        }
    }()
    return a / b, nil // a/0 panics with "integer divide by zero"
}

func main() {
    r, err := safeDivide(10, 0)
    fmt.Println(r, err) // 0 recovered: runtime error: integer divide by zero
}

Full code: examples/month-01/panic-recover/main.go · Run: go run ./examples/month-01/panic-recover

🏋️ Exercises / Practice

Exercise Status Link
Must-style wrapper that panics on error examples/month-01/panic-recover
Boundary recover → return error examples/month-01/panic-recover

🐛 Mistakes Made

  • Called recover() directly in the function body (not inside a deferred func) — it returned nil and the panic still propagated.
  • Recovered, logged, and swallowed a panic that was actually a real bug — hid the problem. Re-panicked instead.

❓ Open Questions

  • Where exactly should a server boundary recover? (At the top of each request/goroutine, converting to a 500 + logged error — never deeper.)

🧠 Active Recall (answer without looking)

  1. Q: Where must recover() be called to have any effect?

    A Directly inside a deferred function. Called anywhere else it just returns `nil` and the panic keeps unwinding.

  2. Q: When should you panic instead of returning an error?

    A Only for programmer bugs / unrecoverable invariants (e.g. `MustCompile` on a bad constant pattern). Expected, runtime failures should be returned as errors.

🪶 Feynman Reflection

A panic is the emergency stop: it abandons the current work and runs everyone's cleanup (defer) on the way out the door. recover is the one person standing in a defer who can catch the falling program and decide to turn the crisis into a normal "here's an error" instead. You pull the emergency stop only for genuine emergencies — broken assumptions, not bad user input.

🕳️ Knowledge Gaps

  • Goroutine-local recovery patterns — will solidify in the concurrency month.

✅ Summary

I understand the panic → defer-unwind → recover mechanism, can convert a panic into an error at a boundary, and I reserve panic for programmer bugs while using errors for everything expected.

⏭️ Next Steps / Prep for Tomorrow

  • Day 021: review the whole week and do closed-book recall.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: feat(examples): panic, recover, and error boundaries (day 020)