DEV Community

kingyou
kingyou

Posted on

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

  1. Understanding Defer
  2. Working with Panic
  3. Recovering from Panics
  4. Best Practices
  5. 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")
}
Enter fullscreen mode Exit fullscreen mode

Output:

Hello
World
Enter fullscreen mode Exit fullscreen mode

Key Characteristics of Defer

  1. LIFO Order: Multiple deferred calls are executed in Last-In-First-Out order
  2. Arguments Evaluated Immediately: Arguments to deferred functions are evaluated when defer is called
  3. 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")
}
Enter fullscreen mode Exit fullscreen mode

Output:

Main function
Third
Second
First
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

Output:

Before panic
Deferred function runs even after panic
panic: Something went wrong!
Enter fullscreen mode Exit fullscreen mode

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: %d\n", age)
}

func main() {
    defer fmt.Println("Program cleanup")
    validateAge(25)
    validateAge(-5) // This will panic
}
Enter fullscreen mode Exit fullscreen mode

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 = %d\n", a, b, a/b)
}

func main() {
    safeDivide(10, 2)
    safeDivide(10, 0) // This panics but is recovered
    fmt.Println("Program continues after recovery")
}
Enter fullscreen mode Exit fullscreen mode

Output:

10 / 2 = 5
Recovered from panic: division by zero
Program continues after recovery
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Best Practices

DO:

  1. 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
}
Enter fullscreen mode Exit fullscreen mode
  1. Use panic for truly exceptional situations
func initialize() {
    if !criticalResourceAvailable() {
        panic("Cannot start: critical resource unavailable")
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. 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()
}
Enter fullscreen mode Exit fullscreen mode

DON'T:

  1. 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
}
Enter fullscreen mode Exit fullscreen mode
  1. 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)
    }
}()
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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: %v\n", 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)
}
Enter fullscreen mode Exit fullscreen mode

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: %v\n", 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 successfully\n", i)
        })
    }

    time.Sleep(time.Second)
    fmt.Println("Main function continues")
}
Enter fullscreen mode Exit fullscreen mode

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! 🚀

Top comments (0)