Web Analytics

Panic & Recover

Intermediate ~30 min read

While Go prefers explicit error handling, it provides panic and recover for exceptional situations. In this lesson, you'll learn when to use panic, how to recover from panics, and best practices for error handling in Go.

What is Panic?

A panic is a runtime error that stops the normal execution of a program. When a function panics:

  1. The function stops executing
  2. Deferred functions are executed
  3. The panic propagates up the call stack
  4. The program crashes (unless recovered)
Output
Click Run to execute your code
Important: Panic is not the same as exceptions in other languages. In Go, use errors for expected failures and panic only for truly exceptional situations!

When to Use Panic

Use Panic For Use Error For
Unrecoverable errors (out of memory) Expected failures (file not found)
Programming errors (nil pointer) Validation errors
Initialization failures Network errors
Impossible situations User input errors

Recovering from Panic

The recover function stops a panic and returns the value passed to panic:

Output
Click Run to execute your code
Recover Rules:
  • recover only works inside deferred functions
  • Returns nil if there's no panic
  • Returns the panic value if recovering from a panic
  • Stops the panic from propagating

Panic vs Errors

Go philosophy: Errors are values, not exceptions

❌ Don't Use Panic

// Bad: Using panic for expected errors
func divide(a, b float64) float64 {
    if b == 0 {
        panic("division by zero")  // Don't do this!
    }
    return a / b
}

βœ… Use Errors Instead

// Good: Return error for expected failures
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Result:", result)
}
Best Practice: Use errors for all expected failures. Reserve panic for situations where the program cannot continue (programming bugs, impossible states).

Practical Patterns

1. Protecting Against Panics in Goroutines

func safeGoroutine(fn func()) {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("Recovered in goroutine:", r)
            }
        }()
        fn()
    }()
}

func main() {
    safeGoroutine(func() {
        panic("Something went wrong!")
    })
    
    time.Sleep(time.Second)
    fmt.Println("Main continues...")
}

2. Cleanup with Defer and Recover

func processFile(filename string) (err error) {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    
    defer func() {
        f.Close()
        if r := recover(); r != nil {
            err = fmt.Errorf("panic: %v", r)
        }
    }()
    
    // Process file (might panic)
    // ...
    
    return nil
}

3. Must Functions (Initialization)

// Common pattern: "Must" functions panic on error
func MustCompile(pattern string) *regexp.Regexp {
    re, err := regexp.Compile(pattern)
    if err != nil {
        panic(err)
    }
    return re
}

// Used during initialization
var emailRegex = MustCompile(`^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$`)

Understanding Stack Traces

When a panic occurs, Go prints a stack trace:

func level3() {
    panic("Something went wrong!")
}

func level2() {
    level3()
}

func level1() {
    level2()
}

func main() {
    level1()
}

// Output shows the call stack:
// panic: Something went wrong!
// 
// goroutine 1 [running]:
// main.level3()
//     /path/to/file.go:4
// main.level2()
//     /path/to/file.go:8
// main.level1()
//     /path/to/file.go:12
// main.main()
//     /path/to/file.go:16

Common Mistakes

1. Using panic for normal errors

// ❌ Wrong - panic for expected errors
func readConfig(filename string) Config {
    data, err := os.ReadFile(filename)
    if err != nil {
        panic(err)  // Don't panic for file errors!
    }
    // ...
}

// βœ… Correct - return error
func readConfig(filename string) (Config, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return Config{}, err
    }
    // ...
}

2. Recover outside defer

// ❌ Wrong - recover doesn't work here
func bad() {
    if r := recover(); r != nil {
        fmt.Println("Recovered:", r)
    }
    panic("Error!")
}

// βœ… Correct - recover in defer
func good() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()
    panic("Error!")
}

3. Swallowing panics silently

// ❌ Wrong - hiding panics
func bad() {
    defer func() {
        recover()  // Silently ignores panic!
    }()
    // ...
}

// βœ… Correct - log or handle panic
func good() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("Panic recovered: %v", r)
            // Take appropriate action
        }
    }()
    // ...
}

Exercise: Safe Division Calculator

Task: Create a safe calculator that handles panics gracefully.

Requirements:

  • Create a safeDivide function that uses recover
  • Handle division by zero with recover
  • Return both result and error
  • Test with various inputs including zero
Show Solution
package main

import (
    "fmt"
)

// safeDivide performs division with panic recovery
func safeDivide(a, b float64) (result float64, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic recovered: %v", r)
        }
    }()
    
    if b == 0 {
        panic("division by zero")
    }
    
    result = a / b
    return result, nil
}

// Better approach: use errors instead of panic
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide by zero")
    }
    return a / b, nil
}

func main() {
    // Test safeDivide
    fmt.Println("=== Using safeDivide (with panic/recover) ===")
    
    result, err := safeDivide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Printf("10 / 2 = %.2f\n", result)
    }
    
    result, err = safeDivide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Printf("10 / 0 = %.2f\n", result)
    }
    
    // Test divide (better approach)
    fmt.Println("\n=== Using divide (with errors) ===")
    
    result, err = divide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Printf("10 / 2 = %.2f\n", result)
    }
    
    result, err = divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Printf("10 / 0 = %.2f\n", result)
    }
}

Summary

  • panic stops normal execution and unwinds the stack
  • recover catches panics and returns the panic value
  • recover only works inside deferred functions
  • Use errors, not panic for expected failures
  • Panic is for exceptional situations only
  • Deferred functions run even during a panic
  • Must functions use panic for initialization errors
  • Always log recovered panics for debugging

What's Next?

Congratulations on completing the Functions module! You now understand functions, closures, defer, panic, and recover. Next, you'll learn about Data Structures, starting with arrays and slicesβ€”Go's powerful collection types.