Panic and Recover in Golang

Panic and Recover in Golang

Panic and recover are Go’s mechanism for handling exceptional situations that cannot be handled through normal error handling. While Go encourages explicit error handling with return values, panic and recover provide a way to handle truly exceptional circumstances.

What is Panic?

Panic is a built-in function that stops the normal execution of a goroutine and begins panicking. When a panic occurs, the function stops executing, any deferred functions are run, and then the panic is propagated up the call stack.

basic-panic.go
package main

import "fmt"

func main() {
    fmt.Println("Starting program")
    
    panic("Something went wrong!")
    
    fmt.Println("This will never be printed")
}

Output:

Starting program
panic: Something went wrong!

goroutine 1 [running]:
main.main()
	/path/to/file.go:7 +0x...

What is Recover?

Recover is a built-in function that regains control of a panicking goroutine. It can only be called from within a deferred function, and it returns the value that was passed to the panic call.

basic-recover.go
package main

import "fmt"

func main() {
    fmt.Println("Starting program")
    
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    
    panic("Something went wrong!")
    
    fmt.Println("This will never be printed")
}

Output:

Starting program
Recovered from panic: Something went wrong!

When to Use Panic

Panic should be used sparingly. According to the Go documentation, panic is for “truly exceptional” situations. Some appropriate uses:

  1. Programming errors - When your code is in an inconsistent state
  2. Initialization failures - When a package cannot be initialized properly
  3. Impossible situations - When something that should never happen occurs
appropriate-panic.go
package main

import (
    "fmt"
    "os"
)

// Check if required environment variable is set
func init() {
    if os.Getenv("REQUIRED_VAR") == "" {
        panic("REQUIRED_VAR environment variable must be set")
    }
}

func divide(a, b int) int {
    if b == 0 {
        panic("division by zero")
    }
    return a / b
}

func main() {
    fmt.Println("Program starting...")
    
    result := divide(10, 2)
    fmt.Println("Result:", result)
    
    // This would panic
    // divide(10, 0)
}

Recovering from Panics

Recover must be called from within a deferred function. If recover is called when not panicking, it returns nil.

recover-example.go
package main

import "fmt"

func safeFunction() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in safeFunction:", r)
        }
    }()
    
    fmt.Println("About to panic")
    panic("Test panic")
    fmt.Println("This won't execute")
}

func main() {
    fmt.Println("Main started")
    safeFunction()
    fmt.Println("Main finished")
}

Panic Propagation

If a panic is not recovered, it propagates up the call stack:

panic-propagation.go
package main

import "fmt"

func level3() {
    panic("Panic from level 3")
}

func level2() {
    level3()
    fmt.Println("Level 2 finished")
}

func level1() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in level 1:", r)
        }
    }()
    
    level2()
    fmt.Println("Level 1 finished")
}

func main() {
    fmt.Println("Main started")
    level1()
    fmt.Println("Main finished")
}

Output:

Main started
Recovered in level 1: Panic from level 3
Main finished

Partial Recovery

You can recover from a panic but still re-panic if needed:

partial-recovery.go
package main

import "fmt"

func processData(data string) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
            // Log the error, cleanup, etc.
            
            // Re-panic if it's not a handled error
            if r != "expected error" {
                panic(r)
            }
        }
    }()
    
    if data == "bad" {
        panic("bad data")
    }
    
    if data == "expected" {
        panic("expected error")
    }
    
    fmt.Println("Processed:", data)
}

func main() {
    fmt.Println("Testing partial recovery")
    
    processData("good")    // No panic
    processData("expected") // Recovered
    processData("bad")     // Re-panics
}

Built-in Recover

Some Go functions use panic/recover internally. For example, the regexp.MustCompile function:

builtin-recover.go
package main

import (
    "fmt"
    "regexp"
)

func main() {
    // This would panic if the regex is invalid
    // validRegex := regexp.MustCompile(`[a-z`)
    
    // This is safe
    regex, err := regexp.Compile(`[a-z]+`)
    if err != nil {
        fmt.Println("Regex compilation failed:", err)
        return
    }
    
    fmt.Println("Regex compiled successfully:", regex.String())
}

Custom Panic Types

You can panic with any type, not just strings:

custom-panic.go
package main

import "fmt"

// Custom error type
type CustomError struct {
    Code    int
    Message string
}

func (e CustomError) Error() string {
    return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
}

func riskyOperation() {
    panic(CustomError{
        Code:    500,
        Message: "Internal server error",
    })
}

func main() {
    defer func() {
        if r := recover(); r != nil {
            switch err := r.(type) {
            case CustomError:
                fmt.Printf("Custom error recovered: %s\n", err.Error())
            default:
                fmt.Printf("Unknown panic: %v\n", r)
                panic(r) // Re-panic unknown errors
            }
        }
    }()
    
    riskyOperation()
    fmt.Println("Operation completed")
}

HTTP Server Example

A practical example of using panic/recover in an HTTP server:

http-server.go
package main

import (
    "fmt"
    "net/http"
)

func panicHandler(w http.ResponseWriter, r *http.Request) {
    // Simulate a panic
    panic("Something went terribly wrong!")
}

func recoveryMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if r := recover(); r != nil {
                fmt.Printf("Panic recovered: %v\n", r)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        
        next(w, r)
    }
}

func main() {
    http.HandleFunc("/panic", recoveryMiddleware(panicHandler))
    http.HandleFunc("/safe", recoveryMiddleware(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "This is safe")
    }))
    
    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", nil)
}

Testing with Panics

You can test code that panics using Go’s testing framework:

panic_test.go
package main

import "testing"

func TestPanicRecovery(t *testing.T) {
    func() {
        defer func() {
            if r := recover(); r != nil {
                t.Logf("Recovered from panic: %v", r)
            }
        }()
        
        panic("test panic")
    }()
}

func TestExpectedPanic(t *testing.T) {
    assertPanic := func(f func()) {
        defer func() {
            if r := recover(); r == nil {
                t.Error("Expected panic but didn't get one")
            }
        }()
        f()
    }
    
    assertPanic(func() {
        divide(10, 0)
    })
}

func divide(a, b int) int {
    if b == 0 {
        panic("division by zero")
    }
    return a / b
}

Best Practices

  1. Don’t use panic for normal errors - Use error return values instead
  2. Recover at appropriate boundaries - Usually at the top level of a package or goroutine
  3. Log recovered panics - Always log when you recover from a panic for debugging
  4. Re-panic unknown panics - If you don’t know how to handle a panic, re-panic it
  5. Use panic in initialization - It’s acceptable to panic during package initialization
  6. Document panicking behavior - If a function can panic, document it
  7. Test panic recovery - Write tests for panic recovery code

Common Patterns

Graceful Degradation

graceful-degradation.go
package main

import "fmt"

func processWithFallback(data string) string {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Processing failed, using fallback: %v\n", r)
        }
    }()
    
    // Try to process data
    if len(data) == 0 {
        panic("empty data")
    }
    
    return fmt.Sprintf("Processed: %s", data)
}

func main() {
    result1 := processWithFallback("valid data")
    fmt.Println(result1)
    
    result2 := processWithFallback("") // This will panic and recover
    fmt.Println("Fallback result:", result2)
}

Resource Cleanup on Panic

cleanup-on-panic.go
package main

import (
    "fmt"
    "os"
)

func processFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    
    // Ensure cleanup even if panic occurs
    defer func() {
        file.Close()
        if r := recover(); r != nil {
            fmt.Printf("Panic occurred, but file was closed: %v\n", r)
            panic(r) // Re-panic after cleanup
        }
    }()
    
    // Simulate processing that might panic
    if filename == "bad.txt" {
        panic("corrupt file")
    }
    
    // Process file...
    return nil
}

func main() {
    err := processFile("good.txt")
    if err != nil {
        fmt.Println("Error:", err)
    }
    
    // This will panic but the file will still be closed
    // processFile("bad.txt")
}

Panic in Goroutines

goroutine-panic.go
package main

import (
    "fmt"
    "time"
)

func worker(id int) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Worker %d recovered from panic: %v\n", id, r)
        }
    }()
    
    if id == 2 {
        panic(fmt.Sprintf("Worker %d encountered an error", id))
    }
    
    fmt.Printf("Worker %d completed successfully\n", id)
}

func main() {
    for i := 1; i <= 3; i++ {
        go worker(i)
    }
    
    time.Sleep(time.Second) // Give goroutines time to complete
}

External Resources:

Related Tutorials:

Last updated on