Go Error Handling and Recovery - Building Robust Applications

Learning Objectives: By the end of this lesson, you will be able to:

  • Master Go’s explicit error handling philosophy and patterns
  • Create and use custom error types for specific problem domains
  • Implement error wrapping and unwrapping for better error context
  • Handle panics gracefully with recovery mechanisms
  • Design robust error handling strategies for production applications
  • Debug and trace errors effectively through application layers
  • Apply industry best practices for error management
  • Build fault-tolerant systems with proper error boundaries

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Think about the last time a software application crashed or gave you a cryptic error message. How frustrating was it? Now imagine if every potential problem was clearly communicated with helpful context about what went wrong and how to fix it. That’s the power of Go’s explicit error handling - it forces developers to think about what can go wrong and handle it gracefully.

Lesson Overview

  • Duration: 5-7 hours
  • Prerequisites: Go functions, interfaces, and basic programming concepts
  • Materials Needed: Go development environment, understanding of function returns

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Error Interface and Philosophy - Go’s unique approach to error handling
  2. Custom Error Types - Creating domain-specific error information
  3. Error Wrapping and Context - Adding context while preserving error chains
  4. Panic and Recovery - Handling exceptional situations gracefully
  5. Error Handling Patterns - Industry-standard approaches and best practices
  6. Testing Error Conditions - Ensuring your error handling works correctly
  7. Production Considerations - Logging, monitoring, and debugging strategies
  8. Advanced Techniques - Error boundaries, timeouts, and retry mechanisms

Success Criteria:

By the end of this session, you should be able to design and implement comprehensive error handling strategies that make your Go applications robust and maintainable.

3. Foundation Knowledge (15-20 minutes)

Go’s Error Handling Philosophy

Go takes a unique approach to error handling that differs significantly from exception-based languages. Instead of throwing exceptions, Go uses explicit error returns that must be checked at each call site.

Real-World Analogy: Think of Go’s error handling like a careful craftsperson who checks each step of their work. Rather than hoping nothing goes wrong and dealing with problems later (exceptions), they verify each operation succeeds before proceeding to the next step.

Why This Matters: Explicit error handling provides:

  • Predictable Control Flow: No hidden exceptions that can crash your program
  • Better Error Context: Errors carry information about what went wrong and where
  • Forced Error Consideration: Developers must think about failure cases
  • Performance: No exception stack unwinding overhead
  • Debugging Clarity: Error paths are visible in the code

Core Concepts

The Error Interface

Go’s built-in error interface is simple but powerful:

type error interface {
    Error() string
}

Any type that implements this interface can be used as an error. This simplicity enables powerful error handling patterns.

Error as Values

In Go, errors are values, not exceptions. This means:

  • Explicit Returns: Functions return errors alongside normal values
  • Explicit Checks: Callers must explicitly check for errors
  • Composable: Errors can be inspected, wrapped, and transformed
  • Typed: Different error types can carry different information

Panic and Recovery

While Go prefers explicit error handling, it provides panic/recover for truly exceptional situations:

  • Panic: Stops normal execution, begins unwinding the stack
  • Recovery: Catches panics and resumes normal execution
  • Use Cases: Programming errors, impossible conditions, library boundaries

Key Terminology

  • Error: Any value implementing the error interface
  • Error Wrapping: Adding context to an error while preserving the original
  • Error Unwrapping: Extracting the underlying error from a wrapped error
  • Panic: Abnormal program termination with stack unwinding
  • Recovery: Mechanism to catch and handle panics
  • Error Sentinel: Predefined error values for specific conditions
  • Error Type: Custom struct implementing error interface
  • Error Chain: Series of wrapped errors providing context trail

4. Detailed Explanation and Examples (25-30 minutes)

Understanding Go’s Error Handling Model

Go’s error handling is based on the principle that errors are part of your program’s normal operation. Rather than exceptional control flow, errors represent expected failure conditions that should be handled explicitly.

Why Explicit Error Handling?

  • Reliability: Forces consideration of all failure paths
  • Performance: No hidden stack unwinding costs
  • Clarity: Error handling logic is visible in the code
  • Debugging: Clear error propagation paths
  • Maintenance: Easy to understand what can fail and why

Common Error Handling Patterns

  1. Check and Handle: Immediate error handling at call site
  2. Check and Return: Propagate errors up the call stack
  3. Check and Wrap: Add context while propagating
  4. Check and Log: Log errors for debugging while continuing
  5. Error Accumulation: Collect multiple errors before handling

Practical Applications

Error handling is essential for:

  • File Operations: Handling missing files, permission errors
  • Network Operations: Timeouts, connection failures, protocol errors
  • Database Operations: Connection issues, constraint violations, transaction failures
  • User Input Validation: Format errors, business rule violations
  • External API Calls: Service unavailable, rate limiting, authentication failures

Common Misconceptions

  1. Misconception: Go’s error handling is verbose and cumbersome Reality: Explicit handling leads to more robust and maintainable code

  2. Misconception: You should never use panic in Go Reality: Panic is appropriate for programming errors and impossible conditions

  3. Misconception: All errors should be handled at the lowest level Reality: Often better to propagate errors with context to appropriate handling level

5. Live Coding Demonstration (20-25 minutes)

Example 1: Basic Error Handling Patterns

package main

import (
    "errors"
    "fmt"
    "strconv"
    "strings"
)

// Basic error creation and handling
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// Error handling with multiple return values
func parseAndDivide(aStr, bStr string) (float64, error) {
    a, err := strconv.ParseFloat(aStr, 64)
    if err != nil {
        return 0, fmt.Errorf("failed to parse first number '%s': %w", aStr, err)
    }
    
    b, err := strconv.ParseFloat(bStr, 64)
    if err != nil {
        return 0, fmt.Errorf("failed to parse second number '%s': %w", bStr, err)
    }
    
    result, err := divide(a, b)
    if err != nil {
        return 0, fmt.Errorf("division failed: %w", err)
    }
    
    return result, nil
}

// Sentinel errors for specific conditions
var (
    ErrEmptyInput    = errors.New("input cannot be empty")
    ErrInvalidFormat = errors.New("invalid input format")
    ErrOutOfRange    = errors.New("value out of acceptable range")
)

// Function using sentinel errors
func validateInput(input string) error {
    input = strings.TrimSpace(input)
    
    if input == "" {
        return ErrEmptyInput
    }
    
    if !strings.Contains(input, "=") {
        return ErrInvalidFormat
    }
    
    parts := strings.Split(input, "=")
    if len(parts) != 2 {
        return ErrInvalidFormat
    }
    
    value, err := strconv.Atoi(strings.TrimSpace(parts[1]))
    if err != nil {
        return fmt.Errorf("invalid number in input: %w", err)
    }
    
    if value < 0 || value > 100 {
        return ErrOutOfRange
    }
    
    return nil
}

// Error checking patterns
func demonstrateErrorPatterns() {
    fmt.Println("=== Basic Error Handling Patterns ===")
    
    // Pattern 1: Check and handle immediately
    result, err := divide(10, 2)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    fmt.Printf("10 / 2 = %.2f\n", result)
    
    // Pattern 2: Check and return (propagate)
    result, err = parseAndDivide("10", "0")
    if err != nil {
        fmt.Printf("Parse and divide error: %v\n", err)
    } else {
        fmt.Printf("Result: %.2f\n", result)
    }
    
    // Pattern 3: Multiple error checks
    inputs := []string{"", "invalid", "key=150", "valid=50"}
    
    for _, input := range inputs {
        err := validateInput(input)
        if err != nil {
            // Check for specific error types
            switch {
            case errors.Is(err, ErrEmptyInput):
                fmt.Printf("Input '%s': Please provide input\n", input)
            case errors.Is(err, ErrInvalidFormat):
                fmt.Printf("Input '%s': Expected format 'key=value'\n", input)
            case errors.Is(err, ErrOutOfRange):
                fmt.Printf("Input '%s': Value must be between 0 and 100\n", input)
            default:
                fmt.Printf("Input '%s': Unexpected error: %v\n", input, err)
            }
        } else {
            fmt.Printf("Input '%s': Valid\n", input)
        }
    }
    
    // Pattern 4: Error accumulation
    fmt.Println("\n--- Error Accumulation ---")
    var allErrors []error
    
    testInputs := []string{"", "bad=200", "good=50"}
    for _, input := range testInputs {
        if err := validateInput(input); err != nil {
            allErrors = append(allErrors, fmt.Errorf("input '%s': %w", input, err))
        }
    }
    
    if len(allErrors) > 0 {
        fmt.Printf("Found %d validation errors:\n", len(allErrors))
        for i, err := range allErrors {
            fmt.Printf("  %d. %v\n", i+1, err)
        }
    } else {
        fmt.Println("All inputs valid")
    }
}

func main() {
    demonstrateErrorPatterns()
}

Explanation:

  • Lines 8-12: Basic error creation with errors.New()
  • Lines 15-28: Error propagation with context using fmt.Errorf()
  • Lines 31-35: Sentinel errors for common conditions
  • Lines 65-70: Pattern of checking and handling errors immediately
  • Lines 73-79: Error propagation pattern
  • Lines 82-98: Using errors.Is() to check for specific error types

Example 2: Custom Error Types

package main

import (
    "fmt"
    "net/http"
    "time"
)

// Custom error type with additional context
type ValidationError struct {
    Field   string
    Value   interface{}
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed for field '%s' with value '%v': %s", 
        e.Field, e.Value, e.Message)
}

// Network error with retry information
type NetworkError struct {
    Operation string
    URL       string
    Attempt   int
    Cause     error
    Timestamp time.Time
}

func (e *NetworkError) Error() string {
    return fmt.Sprintf("network operation '%s' failed on attempt %d for URL '%s' at %s: %v",
        e.Operation, e.Attempt, e.URL, e.Timestamp.Format(time.RFC3339), e.Cause)
}

func (e *NetworkError) Unwrap() error {
    return e.Cause
}

// Temporary error interface
type TemporaryError interface {
    error
    Temporary() bool
}

func (e *NetworkError) Temporary() bool {
    return e.Attempt < 3  // Temporary if we haven't exceeded retry limit
}

// Business logic error
type BusinessError struct {
    Code    string
    Message string
    Details map[string]interface{}
}

func (e *BusinessError) Error() string {
    return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}

func (e *BusinessError) GetCode() string {
    return e.Code
}

func (e *BusinessError) GetDetails() map[string]interface{} {
    return e.Details
}

// User registration with custom error types
type User struct {
    ID       int
    Username string
    Email    string
    Age      int
}

func validateUser(user User) error {
    if user.Username == "" {
        return &ValidationError{
            Field:   "username",
            Value:   user.Username,
            Message: "username cannot be empty",
        }
    }
    
    if len(user.Username) < 3 {
        return &ValidationError{
            Field:   "username",
            Value:   user.Username,
            Message: "username must be at least 3 characters",
        }
    }
    
    if user.Age < 0 || user.Age > 150 {
        return &ValidationError{
            Field:   "age",
            Value:   user.Age,
            Message: "age must be between 0 and 150",
        }
    }
    
    // Email validation (simplified)
    if user.Email == "" || !contains(user.Email, "@") {
        return &ValidationError{
            Field:   "email",
            Value:   user.Email,
            Message: "email must be a valid email address",
        }
    }
    
    return nil
}

func contains(s, substr string) bool {
    return len(s) >= len(substr) && 
           len(substr) > 0 && 
           findIndex(s, substr) >= 0
}

func findIndex(s, substr string) int {
    for i := 0; i <= len(s)-len(substr); i++ {
        if s[i:i+len(substr)] == substr {
            return i
        }
    }
    return -1
}

// Business logic with custom errors
func createUser(user User) error {
    // Validate input
    if err := validateUser(user); err != nil {
        return err  // Propagate validation error
    }
    
    // Check business rules
    if user.Username == "admin" || user.Username == "root" {
        return &BusinessError{
            Code:    "RESERVED_USERNAME",
            Message: "Username is reserved and cannot be used",
            Details: map[string]interface{}{
                "attempted_username": user.Username,
                "suggestions":        []string{user.Username + "1", user.Username + "_user"},
            },
        }
    }
    
    // Simulate database check
    if user.ID == 999 {
        return &BusinessError{
            Code:    "USER_EXISTS",
            Message: "User with this ID already exists",
            Details: map[string]interface{}{
                "existing_id": user.ID,
            },
        }
    }
    
    // Simulate successful creation
    fmt.Printf("User created successfully: %+v\n", user)
    return nil
}

// Network operation with custom error
func fetchUserData(userID int) (*User, error) {
    // Simulate network operation
    if userID == 404 {
        return nil, &NetworkError{
            Operation: "GET",
            URL:       fmt.Sprintf("/api/users/%d", userID),
            Attempt:   1,
            Cause:     fmt.Errorf("HTTP %d: Not Found", http.StatusNotFound),
            Timestamp: time.Now(),
        }
    }
    
    if userID == 500 {
        return nil, &NetworkError{
            Operation: "GET",
            URL:       fmt.Sprintf("/api/users/%d", userID),
            Attempt:   3,
            Cause:     fmt.Errorf("HTTP %d: Internal Server Error", http.StatusInternalServerError),
            Timestamp: time.Now(),
        }
    }
    
    // Simulate successful fetch
    return &User{
        ID:       userID,
        Username: fmt.Sprintf("user%d", userID),
        Email:    fmt.Sprintf("user%d@example.com", userID),
        Age:      25,
    }, nil
}

func main() {
    fmt.Println("=== Custom Error Types Demo ===")
    
    // Test validation errors
    invalidUsers := []User{
        {Username: "", Email: "test@example.com", Age: 25},
        {Username: "ab", Email: "test@example.com", Age: 25},
        {Username: "validuser", Email: "invalid-email", Age: 25},
        {Username: "validuser", Email: "test@example.com", Age: -5},
    }
    
    fmt.Println("--- Validation Error Testing ---")
    for i, user := range invalidUsers {
        err := createUser(user)
        if err != nil {
            // Type assertion to get specific error information
            if validationErr, ok := err.(*ValidationError); ok {
                fmt.Printf("Test %d - Validation Error:\n", i+1)
                fmt.Printf("  Field: %s\n", validationErr.Field)
                fmt.Printf("  Value: %v\n", validationErr.Value)
                fmt.Printf("  Message: %s\n", validationErr.Message)
            } else {
                fmt.Printf("Test %d - Other Error: %v\n", i+1, err)
            }
        }
        fmt.Println()
    }
    
    // Test business errors
    fmt.Println("--- Business Error Testing ---")
    businessTestUsers := []User{
        {ID: 1, Username: "admin", Email: "admin@example.com", Age: 30},
        {ID: 999, Username: "duplicate", Email: "dup@example.com", Age: 25},
        {ID: 1, Username: "validuser", Email: "valid@example.com", Age: 25},
    }
    
    for i, user := range businessTestUsers {
        err := createUser(user)
        if err != nil {
            if businessErr, ok := err.(*BusinessError); ok {
                fmt.Printf("Business Error %d:\n", i+1)
                fmt.Printf("  Code: %s\n", businessErr.GetCode())
                fmt.Printf("  Message: %s\n", businessErr.Message)
                fmt.Printf("  Details: %+v\n", businessErr.GetDetails())
            } else {
                fmt.Printf("Other Error %d: %v\n", i+1, err)
            }
        }
        fmt.Println()
    }
    
    // Test network errors
    fmt.Println("--- Network Error Testing ---")
    userIDs := []int{1, 404, 500}
    
    for _, userID := range userIDs {
        user, err := fetchUserData(userID)
        if err != nil {
            fmt.Printf("Fetch User %d Error:\n", userID)
            
            // Check if it's a temporary error
            if tempErr, ok := err.(TemporaryError); ok {
                fmt.Printf("  Temporary: %t\n", tempErr.Temporary())
                if tempErr.Temporary() {
                    fmt.Println("  -> Should retry operation")
                } else {
                    fmt.Println("  -> Should not retry operation")
                }
            }
            
            // Type assertion for network error details
            if netErr, ok := err.(*NetworkError); ok {
                fmt.Printf("  Operation: %s\n", netErr.Operation)
                fmt.Printf("  URL: %s\n", netErr.URL)
                fmt.Printf("  Attempt: %d\n", netErr.Attempt)
                fmt.Printf("  Timestamp: %s\n", netErr.Timestamp.Format(time.RFC3339))
                fmt.Printf("  Underlying cause: %v\n", netErr.Cause)
            }
        } else {
            fmt.Printf("User %d fetched successfully: %+v\n", userID, *user)
        }
        fmt.Println()
    }
}

Example 3: Error Wrapping and Context

package main

import (
    "errors"
    "fmt"
    "strconv"
    "strings"
)

// Simulated data processing pipeline with error wrapping
type DataProcessor struct {
    Name string
}

// Custom error for data processing
type ProcessingError struct {
    Stage   string
    Input   string
    Cause   error
    Context map[string]interface{}
}

func (e *ProcessingError) Error() string {
    return fmt.Sprintf("processing failed at stage '%s' with input '%s': %v", 
        e.Stage, e.Input, e.Cause)
}

func (e *ProcessingError) Unwrap() error {
    return e.Cause
}

// Data validation stage
func validateData(data string) error {
    if data == "" {
        return errors.New("data cannot be empty")
    }
    
    if len(data) < 3 {
        return errors.New("data too short, minimum 3 characters required")
    }
    
    return nil
}

// Data transformation stage
func transformData(data string) (string, error) {
    if err := validateData(data); err != nil {
        return "", fmt.Errorf("validation failed: %w", err)
    }
    
    // Simulate transformation (convert to uppercase and add prefix)
    if strings.Contains(data, "error") {
        return "", errors.New("data contains forbidden keyword 'error'")
    }
    
    transformed := "PROCESSED_" + strings.ToUpper(data)
    return transformed, nil
}

// Data parsing stage
func parseData(data string) (int, error) {
    transformed, err := transformData(data)
    if err != nil {
        return 0, fmt.Errorf("transformation failed: %w", err)
    }
    
    // Extract number from transformed data
    parts := strings.Split(transformed, "_")
    if len(parts) < 2 {
        return 0, errors.New("no parseable number found in transformed data")
    }
    
    // Try to parse the last part as number
    lastPart := parts[len(parts)-1]
    number, err := strconv.Atoi(lastPart)
    if err != nil {
        return 0, fmt.Errorf("failed to parse number from '%s': %w", lastPart, err)
    }
    
    return number, nil
}

// Main processing method with comprehensive error wrapping
func (dp *DataProcessor) ProcessData(input string) (int, error) {
    result, err := parseData(input)
    if err != nil {
        // Wrap with processing context
        return 0, &ProcessingError{
            Stage: "parse",
            Input: input,
            Cause: err,
            Context: map[string]interface{}{
                "processor": dp.Name,
                "timestamp": "2025-07-30T10:00:00Z",
                "inputLength": len(input),
            },
        }
    }
    
    // Additional business logic validation
    if result < 0 {
        return 0, &ProcessingError{
            Stage: "business_validation",
            Input: input,
            Cause: errors.New("negative numbers not allowed"),
            Context: map[string]interface{}{
                "processor": dp.Name,
                "parsedValue": result,
            },
        }
    }
    
    return result, nil
}

// Error chain analysis
func analyzeError(err error) {
    fmt.Println("=== Error Chain Analysis ===")
    
    // Walk through the error chain
    current := err
    level := 0
    
    for current != nil {
        fmt.Printf("Level %d: %v\n", level, current)
        
        // Check for specific error types
        var processingErr *ProcessingError
        if errors.As(current, &processingErr) {
            fmt.Printf("  -> Processing Error Details:\n")
            fmt.Printf("     Stage: %s\n", processingErr.Stage)
            fmt.Printf("     Input: %s\n", processingErr.Input)
            fmt.Printf("     Context: %+v\n", processingErr.Context)
        }
        
        // Unwrap to next level
        current = errors.Unwrap(current)
        level++
        
        if level > 10 {  // Prevent infinite loops
            fmt.Println("  -> Error chain too deep, stopping analysis")
            break
        }
    }
}

// Batch processing with error collection
func batchProcess(processor *DataProcessor, inputs []string) map[string]interface{} {
    results := make(map[string]interface{})
    var allErrors []error
    
    for _, input := range inputs {
        result, err := processor.ProcessData(input)
        if err != nil {
            results[input] = map[string]interface{}{
                "success": false,
                "error":   err.Error(),
            }
            allErrors = append(allErrors, fmt.Errorf("input '%s': %w", input, err))
        } else {
            results[input] = map[string]interface{}{
                "success": true,
                "result":  result,
            }
        }
    }
    
    // Add summary information
    results["_summary"] = map[string]interface{}{
        "total_inputs":     len(inputs),
        "successful":       len(inputs) - len(allErrors),
        "failed":          len(allErrors),
        "success_rate":    float64(len(inputs)-len(allErrors)) / float64(len(inputs)) * 100,
        "first_error":     nil,
    }
    
    if len(allErrors) > 0 {
        results["_summary"].(map[string]interface{})["first_error"] = allErrors[0].Error()
    }
    
    return results
}

// Error recovery demonstration
func demonstrateErrorRecovery() {
    fmt.Println("\n=== Error Recovery Patterns ===")
    
    processor := &DataProcessor{Name: "MainProcessor"}
    
    // Test different recovery strategies
    inputs := []string{"123", "error_input", "", "abc", "456"}
    
    for _, input := range inputs {
        result, err := processor.ProcessData(input)
        if err != nil {
            fmt.Printf("Processing '%s' failed: %v\n", input, err)
            
            // Recovery strategy 1: Use default value
            defaultResult := 0
            fmt.Printf("  -> Using default value: %d\n", defaultResult)
            
            // Recovery strategy 2: Skip and continue
            fmt.Printf("  -> Skipping this input and continuing\n")
            
            // Recovery strategy 3: Retry with modified input
            if input == "" {
                retryInput := "default"
                fmt.Printf("  -> Retrying with input: '%s'\n", retryInput)
                retryResult, retryErr := processor.ProcessData(retryInput)
                if retryErr == nil {
                    fmt.Printf("  -> Retry successful: %d\n", retryResult)
                } else {
                    fmt.Printf("  -> Retry also failed: %v\n", retryErr)
                }
            }
        } else {
            fmt.Printf("Processing '%s' successful: %d\n", input, result)
        }
        fmt.Println()
    }
}

func main() {
    processor := &DataProcessor{Name: "DemoProcessor"}
    
    // Test successful case
    fmt.Println("=== Successful Processing ===")
    result, err := processor.ProcessData("123")
    if err != nil {
        fmt.Printf("Error: %v\n", err)
    } else {
        fmt.Printf("Success: %d\n", result)
    }
    
    // Test error cases with detailed analysis
    fmt.Println("\n=== Error Cases ===")
    
    errorInputs := []string{"", "xy", "error_test", "abc"}
    
    for _, input := range errorInputs {
        fmt.Printf("\nProcessing input: '%s'\n", input)
        _, err := processor.ProcessData(input)
        if err != nil {
            analyzeError(err)
            
            // Check for specific error conditions
            var processingErr *ProcessingError
            if errors.As(err, &processingErr) {
                fmt.Printf("Found ProcessingError at stage: %s\n", processingErr.Stage)
            }
            
            // Check if error chain contains validation error
            if strings.Contains(err.Error(), "validation failed") {
                fmt.Println("This is a validation-related error")
            }
        }
        fmt.Println(strings.Repeat("-", 50))
    }
    
    // Batch processing demonstration
    fmt.Println("\n=== Batch Processing ===")
    batchInputs := []string{"123", "456", "", "error_input", "789", "abc"}
    results := batchProcess(processor, batchInputs)
    
    for key, value := range results {
        if key == "_summary" {
            fmt.Printf("Summary: %+v\n", value)
        } else {
            fmt.Printf("Input '%s': %+v\n", key, value)
        }
    }
    
    // Error recovery demonstration
    demonstrateErrorRecovery()
}

Example 4: Panic and Recovery

package main

import (
    "fmt"
    "runtime"
    "time"
)

// Demonstration of panic and recovery patterns
type SafeCalculator struct {
    Name string
}

// Function that might panic
func riskyDivision(a, b float64) float64 {
    if b == 0 {
        panic(fmt.Sprintf("cannot divide %.2f by zero", a))
    }
    return a / b
}

// Function with recovery
func (sc *SafeCalculator) SafeDivide(a, b float64) (result float64, err error) {
    // Set up recovery
    defer func() {
        if r := recover(); r != nil {
            // Log panic for debugging
            fmt.Printf("[%s] Panic recovered: %v\n", sc.Name, r)
            
            // Convert panic to error
            err = fmt.Errorf("division panic: %v", r)
            result = 0
        }
    }()
    
    // Call potentially panicking function
    result = riskyDivision(a, b)
    return result, nil
}

// Complex operation with nested panic recovery
func (sc *SafeCalculator) ComplexCalculation(values []float64) (results []float64, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("complex calculation failed: %v", r)
            
            // Log stack trace for debugging
            buf := make([]byte, 1024)
            n := runtime.Stack(buf, false)
            fmt.Printf("Stack trace:\n%s\n", buf[:n])
        }
    }()
    
    results = make([]float64, 0, len(values))
    
    for i, val := range values {
        // Each operation is potentially risky
        intermediate := riskyDivision(val, float64(i))  // Will panic when i=0
        results = append(results, intermediate)
    }
    
    return results, nil
}

// Graceful shutdown with panic recovery
type Server struct {
    Name     string
    IsRunning bool
}

func (s *Server) Start() error {
    s.IsRunning = true
    fmt.Printf("Server %s starting...\n", s.Name)
    
    // Set up panic recovery for the entire server
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Server %s crashed: %v\n", s.Name, r)
            s.gracefulShutdown()
        }
    }()
    
    // Simulate server work that might panic
    s.handleRequests()
    
    return nil
}

func (s *Server) handleRequests() {
    for i := 0; i < 5; i++ {
        fmt.Printf("Handling request %d\n", i+1)
        time.Sleep(100 * time.Millisecond)
        
        // Simulate random panic
        if i == 3 {
            panic("simulated server error: database connection lost")
        }
    }
    
    fmt.Printf("Server %s completed all requests\n", s.Name)
}

func (s *Server) gracefulShutdown() {
    fmt.Printf("Performing graceful shutdown for server %s\n", s.Name)
    s.IsRunning = false
    
    // Cleanup operations
    fmt.Println("- Closing database connections")
    fmt.Println("- Saving pending data")
    fmt.Println("- Notifying monitoring systems")
    fmt.Println("Graceful shutdown completed")
}

// Goroutine panic handling
func riskyGoroutine(id int, resultChan chan<- string, errorChan chan<- error) {
    defer func() {
        if r := recover(); r != nil {
            errorChan <- fmt.Errorf("goroutine %d panicked: %v", id, r)
        }
    }()
    
    // Simulate some work
    time.Sleep(time.Duration(id*100) * time.Millisecond)
    
    // Some goroutines will panic
    if id%3 == 0 {
        panic(fmt.Sprintf("goroutine %d encountered fatal error", id))
    }
    
    resultChan <- fmt.Sprintf("goroutine %d completed successfully", id)
}

func demonstrateGoroutinePanicHandling() {
    fmt.Println("\n=== Goroutine Panic Handling ===")
    
    numGoroutines := 6
    resultChan := make(chan string, numGoroutines)
    errorChan := make(chan error, numGoroutines)
    
    // Start goroutines
    for i := 1; i <= numGoroutines; i++ {
        go riskyGoroutine(i, resultChan, errorChan)
    }
    
    // Collect results and errors
    var results []string
    var errors []error
    
    for i := 0; i < numGoroutines; i++ {
        select {
        case result := <-resultChan:
            results = append(results, result)
        case err := <-errorChan:
            errors = append(errors, err)
        case <-time.After(1 * time.Second):
            fmt.Printf("Timeout waiting for goroutine %d\n", i+1)
        }
    }
    
    fmt.Printf("Successful results: %d\n", len(results))
    for _, result := range results {
        fmt.Printf("  - %s\n", result)
    }
    
    fmt.Printf("Errors encountered: %d\n", len(errors))
    for _, err := range errors {
        fmt.Printf("  - %v\n", err)
    }
}

// Recovery middleware pattern
type Handler func() error

func withRecovery(handler Handler, name string) error {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Handler '%s' recovered from panic: %v\n", name, r)
        }
    }()
    
    return handler()
}

func panickyHandler() error {
    fmt.Println("Doing some work...")
    panic("something went terribly wrong")
    return nil
}

func normalHandler() error {
    fmt.Println("Normal handler executed successfully")
    return nil
}

func main() {
    fmt.Println("=== Panic and Recovery Demonstration ===")
    
    // Basic panic recovery
    calculator := &SafeCalculator{Name: "MainCalc"}
    
    fmt.Println("--- Safe Division Tests ---")
    testCases := [][]float64{
        {10, 2},   // Normal case
        {15, 0},   // Division by zero (will panic)
        {20, 4},   // Normal case
    }
    
    for _, test := range testCases {
        result, err := calculator.SafeDivide(test[0], test[1])
        if err != nil {
            fmt.Printf("Error: %.2f / %.2f -> %v\n", test[0], test[1], err)
        } else {
            fmt.Printf("Success: %.2f / %.2f = %.2f\n", test[0], test[1], result)
        }
    }
    
    // Complex calculation with panic
    fmt.Println("\n--- Complex Calculation Test ---")
    values := []float64{10, 20, 30, 40}  // Will panic on first iteration (divide by 0)
    results, err := calculator.ComplexCalculation(values)
    if err != nil {
        fmt.Printf("Complex calculation failed: %v\n", err)
    } else {
        fmt.Printf("Complex calculation results: %v\n", results)
    }
    
    // Server panic recovery
    fmt.Println("\n--- Server Panic Recovery ---")
    server := &Server{Name: "WebServer"}
    err := server.Start()
    if err != nil {
        fmt.Printf("Server error: %v\n", err)
    }
    
    // Goroutine panic handling
    demonstrateGoroutinePanicHandling()
    
    // Recovery middleware pattern
    fmt.Println("\n--- Recovery Middleware Pattern ---")
    
    handlers := map[string]Handler{
        "normal":  normalHandler,
        "panicky": panickyHandler,
    }
    
    for name, handler := range handlers {
        fmt.Printf("Executing handler '%s':\n", name)
        err := withRecovery(handler, name)
        if err != nil {
            fmt.Printf("Handler returned error: %v\n", err)
        }
        fmt.Println()
    }
    
    // Best practices summary
    fmt.Println("=== Panic and Recovery Best Practices ===")
    fmt.Println("1. Use panic only for programming errors or impossible conditions")
    fmt.Println("2. Always use defer for recovery to ensure cleanup")
    fmt.Println("3. Convert panics to errors at package boundaries")
    fmt.Println("4. Log panics with stack traces for debugging")
    fmt.Println("5. Implement graceful shutdown in long-running processes")
    fmt.Println("6. Handle panics in goroutines to prevent program termination")
}

6. Hands-On Practice (30-40 minutes)

Exercise 1: File Processing with Error Handling (10 minutes)

Objective: Build a robust file processor with comprehensive error handling

Instructions:

  1. Create a file reader that handles missing files, permission errors
  2. Implement data validation with custom error types
  3. Add error wrapping with context
  4. Include recovery mechanisms for critical errors

Starter Code:

package main

import (
    "fmt"
    "os"
)

type FileProcessor struct {
    Name string
}

// TODO: Implement ProcessFile method with error handling
// TODO: Create custom error types for different failure modes
// TODO: Add error wrapping and context
// TODO: Handle panics gracefully

func main() {
    processor := &FileProcessor{Name: "DataProcessor"}
    
    // Test with various file scenarios
    files := []string{"existing.txt", "missing.txt", "restricted.txt"}
    
    for _, filename := range files {
        fmt.Printf("Processing %s...\n", filename)
        // Call your ProcessFile method
    }
}

Solution:

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

type FileProcessor struct {
    Name string
}

// Custom error types
type FileError struct {
    Operation string
    Filename  string
    Cause     error
}

func (e *FileError) Error() string {
    return fmt.Sprintf("file operation '%s' failed for '%s': %v", 
        e.Operation, e.Filename, e.Cause)
}

func (e *FileError) Unwrap() error {
    return e.Cause
}

type ValidationError struct {
    Line     int
    Content  string
    Expected string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed at line %d: expected %s, got '%s'", 
        e.Line, e.Expected, e.Content)
}

// ProcessFile with comprehensive error handling
func (fp *FileProcessor) ProcessFile(filename string) ([]int, error) {
    // Set up panic recovery
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("[%s] Recovered from panic while processing %s: %v\n", 
                fp.Name, filename, r)
        }
    }()
    
    // Open file with error wrapping
    file, err := os.Open(filename)
    if err != nil {
        return nil, &FileError{
            Operation: "open",
            Filename:  filename,
            Cause:     err,
        }
    }
    defer file.Close()
    
    var numbers []int
    scanner := bufio.NewScanner(file)
    lineNum := 0
    
    for scanner.Scan() {
        lineNum++
        line := strings.TrimSpace(scanner.Text())
        
        if line == "" {
            continue // Skip empty lines
        }
        
        number, err := strconv.Atoi(line)
        if err != nil {
            return nil, &ValidationError{
                Line:     lineNum,
                Content:  line,
                Expected: "integer",
            }
        }
        
        numbers = append(numbers, number)
    }
    
    if err := scanner.Err(); err != nil {
        return nil, &FileError{
            Operation: "read",
            Filename:  filename,
            Cause:     err,
        }
    }
    
    return numbers, nil
}

func main() {
    processor := &FileProcessor{Name: "DataProcessor"}
    
    // Create test files
    testFiles := map[string]string{
        "numbers.txt": "1\n2\n3\n4\n5",
        "mixed.txt":   "1\nabc\n3",
        "empty.txt":   "",
    }
    
    for filename, content := range testFiles {
        file, err := os.Create(filename)
        if err != nil {
            fmt.Printf("Failed to create test file %s: %v\n", filename, err)
            continue
        }
        file.WriteString(content)
        file.Close()
    }
    
    // Test processing
    files := []string{"numbers.txt", "mixed.txt", "empty.txt", "missing.txt"}
    
    for _, filename := range files {
        fmt.Printf("Processing %s...\n", filename)
        numbers, err := processor.ProcessFile(filename)
        
        if err != nil {
            // Handle different error types
            var fileErr *FileError
            var validationErr *ValidationError
            
            switch {
            case errors.As(err, &fileErr):
                fmt.Printf("  File Error: %s\n", err)
                if os.IsNotExist(fileErr.Cause) {
                    fmt.Printf("  -> File doesn't exist, creating default...\n")
                    // Recovery: create default file
                }
            case errors.As(err, &validationErr):
                fmt.Printf("  Validation Error: %s\n", err)
                fmt.Printf("  -> Skipping invalid line and continuing...\n")
            default:
                fmt.Printf("  Unknown Error: %s\n", err)
            }
        } else {
            fmt.Printf("  Success: %v\n", numbers)
        }
        fmt.Println()
    }
    
    // Cleanup
    for filename := range testFiles {
        os.Remove(filename)
    }
}

Exercise 2: HTTP Client with Retry Logic (15 minutes)

Objective: Implement an HTTP client with sophisticated error handling and retry mechanisms

Requirements:

  • Handle different types of HTTP errors (timeout, 4xx, 5xx)
  • Implement exponential backoff for retries
  • Create custom error types with retry information
  • Add circuit breaker pattern for repeated failures

Instructions:

  1. Design error types for different HTTP failure scenarios
  2. Implement retry logic with exponential backoff
  3. Add circuit breaker to prevent cascading failures
  4. Include comprehensive logging and monitoring

Exercise 3: Concurrent Error Aggregation (15 minutes)

Objective: Build a system that handles errors from multiple concurrent operations

Challenge: Create a worker pool that processes tasks concurrently and aggregates all errors

Requirements:

  • Process multiple tasks concurrently using goroutines
  • Collect and categorize errors from all workers
  • Implement timeout handling for long-running tasks
  • Provide detailed error reporting and statistics

Extension Ideas:

  • Add different error recovery strategies per error type
  • Implement error rate limiting to prevent spam
  • Create error dashboards with metrics
  • Add integration with external monitoring systems

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Ignoring errors or using _ to discard them Cause: Laziness or not understanding the importance of error handling Solution: Always handle errors appropriately - check, log, or propagate

  2. Problem: Creating error strings that are hard to parse programmatically Cause: Using only string errors without structure Solution: Use custom error types with structured information

  3. Problem: Losing error context when propagating up the call stack Cause: Not using error wrapping to preserve context Solution: Use fmt.Errorf() with %w verb to wrap errors

  4. Problem: Overusing panic for normal error conditions Cause: Misunderstanding when panic is appropriate Solution: Use panic only for programming errors and impossible conditions

Debugging Tips

  • Error Logging: Log errors with sufficient context (timestamps, user IDs, request IDs)
  • Error Metrics: Track error rates and types for monitoring
  • Error Tracing: Use correlation IDs to trace errors across services
  • Stack Traces: Include stack traces for debugging but not for user-facing errors

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Error Messages: Clear, actionable, and user-appropriate
  • Error Codes: Consistent error codes for API responses
  • Error Documentation: Document all possible errors for public APIs
  • Error Testing: Write tests for all error conditions

Code Organization

// Good: Structured error with context
type APIError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Details string `json:"details,omitempty"`
}

// Good: Error checking pattern
result, err := operation()
if err != nil {
    return fmt.Errorf("operation failed: %w", err)
}

Performance Considerations

  • Error Allocation: Pre-allocate common errors to reduce GC pressure
  • Error Strings: Use error variables instead of creating new strings each time
  • Error Propagation: Consider performance impact of deep error wrapping

Next Steps in Your Learning Journey

  • Context Package: Learn timeout and cancellation patterns
  • Logging Libraries: Structured logging with error correlation
  • Monitoring: Error tracking and alerting systems
  • Distributed Tracing: Error tracking across microservices

9. Summary and Review (5 minutes)

Key Takeaways

  1. Explicit Errors: Go’s error handling makes failure cases visible and manageable
  2. Custom Types: Rich error types provide better context and handling options
  3. Error Wrapping: Preserve error chains while adding context
  4. Panic Recovery: Use for exceptional cases with proper recovery mechanisms

Quick Review Questions

  1. When should you use custom error types vs simple string errors?
  2. How does error wrapping help with debugging and error handling?
  3. What’s the difference between panic/recover and normal error handling?
  4. How can you check if an error is of a specific type or contains another error?

Answers:

  1. Use custom types when you need structured information or type-specific handling
  2. Wrapping preserves error chains while adding context at each layer
  3. Panic/recover is for exceptional cases; normal errors are for expected failures
  4. Use errors.Is() and errors.As() for type checking and unwrapping

For Instructors

Teaching Tips

  • Demonstrate real-world scenarios where poor error handling causes production issues
  • Show debugging sessions where good error messages save time
  • Compare with other languages to highlight Go’s approach
  • Emphasize testing error conditions as much as success paths

Assessment Ideas

  • Error handling refactoring: Give code with poor error handling to improve
  • Error type design: Create custom errors for specific business domains
  • Debugging exercises: Trace errors through complex call stacks
  • Production scenarios: Handle cascading failures and recovery

Adaptation Notes

  • Beginner Focus: Start with basic error checking patterns
  • Advanced Students: Explore error monitoring and distributed tracing
  • Coming from Exceptions: Highlight differences and advantages
  • Time Management: Error handling concepts build on each other progressively