panic, recover, and Failure Modes

Updated

July 30, 2026

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)  // panic

Recover

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.