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
panicis an abnormal termination signalβ do not use for normal errorsrecoveronly works insidedeferfunctions- Libraries must convert internal panics to
errorbefore exposing to API users- Appropriate uses for
panic: programming errors, init failures, unrecoverable situations- The
Mustpattern is only for the initialization phase (not during runtime)deferruns in LIFO order and executes even when a panic occurs