Skip to main content

panic and recover

panic occurs when the program cannot continue normally. While Go favors returning error over exceptions, panic is appropriate for unrecoverable situations or programming errors. recover only works inside defer functions and safely handles panics.

panic Basics​

package main

import "fmt"

func riskyOperation(n int) {
if n == 0 {
panic("n cannot be zero") // panic with a string
}
if n < 0 {
panic(fmt.Sprintf("n must be positive, got %d", n)) // formatted panic
}
fmt.Println("result:", 100/n)
}

func accessSlice(s []int, i int) int {
// Out-of-bounds index β€” Go runtime automatically panics
return s[i]
}

func main() {
// Normal operation
riskyOperation(5) // result: 20

// Runtime panic examples (uncomment to try)
// s := []int{1, 2, 3}
// fmt.Println(accessSlice(s, 10)) // index out of range [10] with length 3

// Panic β€” code below this point never executes
// riskyOperation(0) // goroutine 1 [running]: main.riskyOperation(...)

fmt.Println("program exits normally")
}

Recovering from Panics with recover​

recover can only catch panics inside a defer function.

package main

import (
"fmt"
"runtime/debug"
)

// Wrapper that converts panic to error
func safeCall(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
fn()
return nil
}

// Version with stack trace
func safeCallWithStack(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
err = fmt.Errorf("panic recovered: %v\nstack:\n%s", r, stack)
}
}()
fn()
return nil
}

func dangerousFunc() {
panic("something went terribly wrong")
}

func nilPointerFunc() {
var p *int
_ = *p // nil pointer dereference
}

func indexOutOfRangeFunc() {
s := []int{1, 2, 3}
_ = s[10] // index out of range
}

func main() {
// Recover panics with safeCall
testCases := []struct {
name string
fn func()
}{
{"dangerousFunc", dangerousFunc},
{"nilPointerFunc", nilPointerFunc},
{"indexOutOfRangeFunc", indexOutOfRangeFunc},
{"normal function", func() { fmt.Println(" β†’ normal execution") }},
}

for _, tc := range testCases {
fmt.Printf("=== %s ===\n", tc.name)
if err := safeCall(tc.fn); err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Println("success")
}
}
}

panic Policy in Libraries​

When writing libraries, internal panics must always be recovered so they don't propagate externally.

package main

import (
"errors"
"fmt"
)

// Pattern for converting internal panics to errors in library code
// (typical library code style)

// JSONParser β€” uses panic internally but converts to error for external API
type JSONParser struct{}

// Internal β€” uses panic (for performance/convenience)
func (p *JSONParser) parseValue(data string, pos int) (any, int) {
if pos >= len(data) {
panic(fmt.Sprintf("unexpected end of input at position %d", pos))
}
switch data[pos] {
case '"':
// String parsing
end := pos + 1
for end < len(data) && data[end] != '"' {
end++
}
if end >= len(data) {
panic("unterminated string")
}
return data[pos+1 : end], end + 1
case 't':
if pos+4 <= len(data) && data[pos:pos+4] == "true" {
return true, pos + 4
}
panic("invalid token")
case 'f':
if pos+5 <= len(data) && data[pos:pos+5] == "false" {
return false, pos + 5
}
panic("invalid token")
default:
panic(fmt.Sprintf("unexpected character %q at position %d", data[pos], pos))
}
}

// Public API β€” converts panic to error (users only see error)
func (p *JSONParser) Parse(data string) (result any, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("json parse error: %v", r)
result = nil
}
}()

if data == "" {
return nil, errors.New("empty input")
}

value, _ := p.parseValue(data, 0)
return value, nil
}

// Calculator β€” internal panic, public error
type Calculator struct{}

func (c *Calculator) mustDivide(a, b float64) float64 {
if b == 0 {
panic("division by zero")
}
return a / b
}

func (c *Calculator) complexCalc(a, b, c2 float64) float64 {
// Internal operation chain β€” using panic simplifies error checking
step1 := c.mustDivide(a, b)
step2 := c.mustDivide(step1, c2)
return step2
}

// Public API
func (c *Calculator) Calculate(a, b, c2 float64) (result float64, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("calculation failed: %v", r)
}
}()
result = c.complexCalc(a, b, c2)
return result, nil
}

func main() {
// JSONParser tests
parser := &JSONParser{}
inputs := []string{
`"hello"`,
`true`,
`false`,
`"unterminated`,
`xyz`,
``,
}

fmt.Println("=== JSON Parser ===")
for _, input := range inputs {
val, err := parser.Parse(input)
if err != nil {
fmt.Printf("parse failed %q: %v\n", input, err)
} else {
fmt.Printf("parse success %q β†’ %v (%T)\n", input, val, val)
}
}

// Calculator tests
fmt.Println("\n=== Calculator ===")
calc := &Calculator{}
cases := [][3]float64{
{100, 5, 4}, // 100 / 5 / 4 = 5
{100, 0, 4}, // divide by zero
{100, 5, 0}, // divide by zero
}
for _, c := range cases {
result, err := calc.Calculate(c[0], c[1], c[2])
if err != nil {
fmt.Printf("calc failed (%.0f, %.0f, %.0f): %v\n", c[0], c[1], c[2], err)
} else {
fmt.Printf("calc result (%.0f, %.0f, %.0f) = %.2f\n", c[0], c[1], c[2], result)
}
}
}

When panic Is Appropriate​

package main

import "fmt"

// Appropriate panic use case 1: programming error β€” prevent incorrect API usage
func NewPositiveInt(n int) int {
if n <= 0 {
// Programmer is misusing the API β€” panic is appropriate
panic(fmt.Sprintf("NewPositiveInt: n must be positive, got %d", n))
}
return n
}

// Appropriate panic use case 2: initialization failure β€” server can't start
func mustLoadConfig(path string) map[string]string {
if path == "" {
panic("config path cannot be empty β€” server cannot start")
}
// In reality, load from file
return map[string]string{"port": "8080", "host": "localhost"}
}

// Must pattern β€” converts error to panic, only for initialization
func Must[T any](val T, err error) T {
if err != nil {
panic(err)
}
return val
}

// Inappropriate panic usage (should return error instead)
// Bad:
func badOpenFile(path string) string {
if path == "" {
panic("path required") // Bad! Normal error condition
}
return "content"
}

// Good:
func goodOpenFile(path string) (string, error) {
if path == "" {
return "", fmt.Errorf("path is required") // Good! Return error
}
return "content", nil
}

func main() {
// Correct usage
n := NewPositiveInt(42)
fmt.Println("positive:", n)

config := mustLoadConfig("config.yaml")
fmt.Println("config:", config)

// Must pattern
// result := Must(strconv.Atoi("123")) β€” use at initialization
// result := Must(strconv.Atoi("abc")) β€” panic!

// Preventing incorrect usage (uncomment to see panic)
// NewPositiveInt(-1) // panic: NewPositiveInt: n must be positive, got -1

fmt.Println("\npanic vs error decision guide:")
fmt.Println("panic β†’ programming error, init failure, unrecoverable situation")
fmt.Println("error β†’ predictable failure (file missing, network error, validation failure)")
}

defer and panic Execution Order​

package main

import "fmt"

func demonstrateOrder() {
defer fmt.Println("defer 1 β€” runs last")
defer fmt.Println("defer 2")
defer fmt.Println("defer 3 β€” runs first (LIFO)")

fmt.Println("function body executing")
// defer is stacked (LIFO)
}

func panicWithDefer() {
defer fmt.Println("defer A β€” runs even after panic!")
defer fmt.Println("defer B β€” runs even after panic!")

fmt.Println("before panic")
panic("test panic")
fmt.Println("this line never executes") // never runs
}

func recoverExample() (result string) {
defer func() {
if r := recover(); r != nil {
fmt.Println("panic recovered:", r)
result = "recovered" // can modify return value
}
}()

panicFunc := func() {
panic("inner panic")
}
panicFunc()
return "normal" // never runs
}

func main() {
fmt.Println("=== defer execution order ===")
demonstrateOrder()

fmt.Println("\n=== panic + defer ===")
// panicWithDefer() // this function has no recover, program exits

fmt.Println("\n=== recover example ===")
result := recoverExample()
fmt.Println("result:", result)
fmt.Println("program continues running...")
}

Key Summary

  • panic is an abnormal termination signalβ€” do not use for normal errors
  • recover only works inside defer functions
  • Libraries must convert internal panics to error before exposing to API users
  • Appropriate uses for panic: programming errors, init failures, unrecoverable situations
  • The Must pattern is only for the initialization phase (not during runtime)
  • defer runs in LIFO order and executes even when a panic occurs