panic, recover, and Failure Modes
Overview
panic and recover handle unrecoverable errors. Unlike normal errors, panics crash the program unless recovered.
When to Panic
✅ Appropriate: - Programmer errors (bugs) - Impossible conditions - Failed invariants - During initialization
❌ Avoid for: - Expected errors (file not found, network timeout) - User input validation - Anything recoverable
Panic
func divide(a, b int) int {
if b == 0 {
panic("division by zero") // Terminates program
}
return a / b
}Common Panic Sources
// nil pointer dereference
var p *int
*p = 1 // panic
// Index out of range
arr := []int{1, 2}
arr[10] // panic
// Invalid type assertion
var i interface{} = "string"
i.(int) // panicRecover
Recover catches panics in deferred functions:
func safeCall() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
dangerousOperation()
return nil
}HTTP Server Pattern
func handler(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %v\n%s", err, debug.Stack())
http.Error(w, "Internal Server Error", 500)
}
}()
// Handle request
}Panic vs Error
// Return error for expected failures
func ReadFile(name string) ([]byte, error) {
if !exists(name) {
return nil, ErrNotFound // Expected case
}
// ...
}
// Panic for bugs
func MustCompile(pattern string) *Regexp {
r, err := Compile(pattern)
if err != nil {
panic(err) // Invalid pattern is programmer error
}
return r
}Summary
| Keyword | Purpose |
|---|---|
panic |
Unrecoverable error (crash) |
recover |
Catch panic in defer |
error |
Expected, recoverable failures |
| Use panic for | Use error for |
|---|---|
| Bugs | Expected failures |
| Invariant violations | User/external input |
| Initialization failures | I/O, network errors |
Worked example
Boundary recover that turns a panic into an error (HTTP/worker style).
Save as main.go. Then:
go mod init example
go run .package main
import (
"fmt"
"runtime/debug"
)
func protect(label string, fn func() error) (err error) {
defer func() {
if r := recover(); r != nil {
// In real servers, log debug.Stack() and return a generic 500.
_ = debug.Stack()
err = fmt.Errorf("%s: panic: %v", label, r)
}
}()
return fn()
}
func parseAge(s string) (int, error) {
if s == "" {
return 0, fmt.Errorf("empty age")
}
// Bug-style panic: index out of range on bad internal invariant.
if s[0] == '!' {
_ = []int{}[1]
}
n := 0
for _, r := range s {
if r < '0' || r > '9' {
return 0, fmt.Errorf("bad digit in %q", s)
}
n = n*10 + int(r-'0')
}
return n, nil
}
func main() {
err := protect("handler", func() error {
age, err := parseAge("34")
if err != nil {
return err
}
fmt.Println("age:", age)
return nil
})
fmt.Println("ok path err:", err)
err = protect("handler", func() error {
_, err := parseAge("")
return err
})
fmt.Println("expected error:", err)
err = protect("handler", func() error {
_, err := parseAge("!boom")
return err
})
fmt.Println("recovered panic:", err)
}Expected output:
age: 34
ok path err: <nil>
expected error: empty age
recovered panic: handler: panic: runtime error: index out of range [1] with length 0
More examples
recover only works on the same goroutine that panics.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
// Wrong: recover in parent does not catch child panic.
// We catch inside the child instead.
wg.Add(1)
go func() {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
fmt.Println("child recovered:", r)
}
}()
panic("child boom")
}()
wg.Wait()
fmt.Println("parent still running")
}Expected output:
child recovered: child boom
parent still running
Runnable example
Save as main.go. Then:
go mod init example
go run .package main
import (
"fmt"
"regexp"
)
func mustCompile(pattern string) *regexp.Regexp {
r, err := regexp.Compile(pattern)
if err != nil {
panic(err) // invalid pattern is a programmer error at init
}
return r
}
func safeCall(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
fn()
return nil
}
func divide(a, b int) int {
if b == 0 {
panic("division by zero")
}
return a / b
}
func main() {
re := mustCompile(`^\d+$`)
fmt.Println("regex match 42:", re.MatchString("42"))
err := safeCall(func() {
fmt.Println("divide:", divide(10, 2))
})
fmt.Println("safe ok:", err)
err = safeCall(func() {
_ = divide(1, 0)
})
fmt.Println("safe recovered:", err)
// Prefer error returns for expected failure modes.
if !re.MatchString("xx") {
fmt.Println("expected validation failure handled without panic")
}
}Expected output:
regex match 42: true
divide: 5
safe ok: <nil>
safe recovered: panic recovered: division by zero
expected validation failure handled without panic
What to notice: recover only works inside deferred functions on the panicking goroutine. Convert panics to errors at boundaries (workers, HTTP handlers); keep normal validation on the error path.
Try next: Panic inside a new goroutine without recover and observe process exit; wrap a handler-style function that always recovers and logs.