Mastering Error Handling in Go: A Deep Dive into Defer, Panic, and Recover
Error handling is a critical aspect of writing robust Go applications. Go provides three powerful mechanisms for managing program flow and errors: defer, panic, and recover. Understanding how these work together is essential for writing reliable, maintainable code.
Table of Contents
- Understanding Defer
- Working with Panic
- Recovering from Panics
- Best Practices
- Real-World Examples
1. Understanding Defer
The defer statement schedules a function call to be executed after the surrounding function returns, regardless of whether it returns normally or due to a panic.
Basic Defer Usage
package main
import "fmt"
func main() {
defer fmt.Println("World")
fmt.Println("Hello")
}
Output:
Hello
World
Key Characteristics of Defer
- LIFO Order: Multiple deferred calls are executed in Last-In-First-Out order
- Arguments Evaluated Immediately: Arguments to deferred functions are evaluated when defer is called
- Always Executes: Runs even if the function panics
Example: Multiple Defers
package main
import "fmt"
func main() {
defer fmt.Println("First")
defer fmt.Println("Second")
defer fmt.Println("Third")
fmt.Println("Main function")
}
Output:
Main function
Third
Second
First
Practical Use Case: Resource Cleanup
package main
import (
"fmt"
"os"
)
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // Ensures file is closed even if error occurs
// Read and process file...
fmt.Println("File processing...")
return nil
}
func main() {
err := readFile("example.txt")
if err != nil {
fmt.Println("Error:", err)
}
}
2. Working with Panic
panic is a built-in function that stops the normal execution flow and begins panicking. When a function panics, it stops executing, runs all deferred functions, and then returns to its caller.
When to Use Panic
Use panic for:
- Unrecoverable errors: Situations where the program cannot continue
- Programming errors: Bugs that should never happen in production
- Initialization failures: Critical setup that must succeed
Basic Panic Example
package main
import "fmt"
func main() {
defer fmt.Println("Deferred function runs even after panic")
fmt.Println("Before panic")
panic("Something went wrong!")
fmt.Println("This will never execute")
}
Output:
Before panic
Deferred function runs even after panic
panic: Something went wrong!
Example: Panic with Custom Error
package main
import "fmt"
func validateAge(age int) {
if age < 0 {
panic("Age cannot be negative!")
}
if age > 150 {
panic("Age is unrealistic!")
}
fmt.Printf("Valid age: %dn", age)
}
func main() {
defer fmt.Println("Program cleanup")
validateAge(25)
validateAge(-5) // This will panic
}
3. Recovering from Panics
recover is a built-in function that regains control of a panicking goroutine. It’s only useful inside deferred functions.
Basic Recovery Pattern
package main
import "fmt"
func safeDivide(a, b int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
if b == 0 {
panic("division by zero")
}
fmt.Printf("%d / %d = %dn", a, b, a/b)
}
func main() {
safeDivide(10, 2)
safeDivide(10, 0) // This panics but is recovered
fmt.Println("Program continues after recovery")
}
Output:
10 / 2 = 5
Recovered from panic: division by zero
Program continues after recovery
Advanced Recovery with Error Return
package main
import (
"errors"
"fmt"
)
func riskyOperation() (err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New(fmt.Sprintf("panic occurred: %v", r))
}
}()
// Simulate a panic
panic("unexpected error")
}
func main() {
err := riskyOperation()
if err != nil {
fmt.Println("Caught error:", err)
}
}
4. Best Practices
DO:
- Use defer for resource cleanup
func processData(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
// Process file
return nil
}
- Use panic for truly exceptional situations
func initialize() {
if !criticalResourceAvailable() {
panic("Cannot start: critical resource unavailable")
}
}
- Recover in top-level functions or middleware
func handleRequest(handler func()) {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from panic: %v", r)
// Return 500 error to client
}
}()
handler()
}
DON’T:
- Don’t use panic for normal error handling
// BAD
func divide(a, b int) int {
if b == 0 {
panic("division by zero")
}
return a / b
}
// GOOD
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
- Don’t ignore deferred function errors
// BAD
defer file.Close()
// GOOD
defer func() {
if err := file.Close(); err != nil {
log.Printf("Error closing file: %v", err)
}
}()
5. Real-World Examples
Example 1: Database Transaction Handling
package main
import (
"database/sql"
"fmt"
)
func performTransaction(db *sql.DB) (err error) {
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if p := recover(); p != nil {
tx.Rollback()
err = fmt.Errorf("transaction panicked: %v", p)
} else if err != nil {
tx.Rollback()
} else {
err = tx.Commit()
}
}()
// Perform database operations
_, err = tx.Exec("INSERT INTO users (name) VALUES (?)", "Alice")
if err != nil {
return err
}
return nil
}
Example 2: HTTP Server Recovery Middleware
package main
import (
"fmt"
"net/http"
)
func recoveryMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
fmt.Printf("Panic: %vn", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next(w, r)
}
}
func riskyHandler(w http.ResponseWriter, r *http.Request) {
// This might panic
panic("oops, something went wrong!")
}
func main() {
http.HandleFunc("/risky", recoveryMiddleware(riskyHandler))
http.ListenAndServe(":8080", nil)
}
Example 3: Goroutine Panic Recovery
package main
import (
"fmt"
"time"
)
func safeGoroutine(id int, task func()) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Goroutine %d recovered from panic: %vn", id, r)
}
}()
task()
}
func main() {
for i := 1; i <= 3; i++ {
go safeGoroutine(i, func() {
if i == 2 {
panic("error in goroutine 2")
}
fmt.Printf("Goroutine %d completed successfullyn", i)
})
}
time.Sleep(time.Second)
fmt.Println("Main function continues")
}
Conclusion
Understanding defer, panic, and recover is crucial for writing robust Go applications:
- Defer ensures cleanup code runs regardless of how a function exits
- Panic should be reserved for truly exceptional situations
- Recover allows you to gracefully handle panics in specific contexts
Remember: In Go, errors are values. Prefer returning errors over panicking whenever possible. Use panic only when the program truly cannot continue, and always recover at appropriate boundaries (like HTTP handlers or goroutine entry points).
Happy coding! 🚀