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.
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.
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:
- Programming errors - When your code is in an inconsistent state
- Initialization failures - When a package cannot be initialized properly
- Impossible situations - When something that should never happen occurs
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.
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:
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 finishedPartial Recovery
You can recover from a panic but still re-panic if needed:
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:
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:
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:
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:
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
- Don’t use panic for normal errors - Use error return values instead
- Recover at appropriate boundaries - Usually at the top level of a package or goroutine
- Log recovered panics - Always log when you recover from a panic for debugging
- Re-panic unknown panics - If you don’t know how to handle a panic, re-panic it
- Use panic in initialization - It’s acceptable to panic during package initialization
- Document panicking behavior - If a function can panic, document it
- Test panic recovery - Write tests for panic recovery code
Common Patterns
Graceful Degradation
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
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
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: