1. go
  2. /best practices
  3. /error-handling

Error Handling Best Practices in Go

Error handling is a critical aspect of writing robust Go applications. This guide covers idiomatic approaches and best practices for handling errors effectively.

Understanding Go's Error Philosophy

Go's approach to error handling is distinct from many other languages:

  1. Explicit Error Handling:

    • Errors are values, not exceptions
    • Errors must be checked explicitly
    • No implicit error propagation
    • Clear error flow in code
  2. Error as Values:

    • Errors are just interfaces
    • Can contain rich context
    • Can be programmatically examined
    • Can be wrapped and unwrapped
  3. Multiple Return Values:

    • Functions return error as last value
    • Separate success and failure paths
    • Clear error propagation
    • Easy to handle errors locally

Basic Error Handling

1. The Error Interface

The foundation of Go's error handling:

type error interface {
    Error() string
}

Creating custom errors:

// Simple string error
errors.New("something went wrong")

// Formatted error
fmt.Errorf("failed to process %s: %w", name, err)

// Custom error type
type ValidationError struct {
    Field string
    Error string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Error)
}

2. Error Checking Patterns

Idiomatic error handling:

// Basic error check
result, err := SomeFunction()
if err != nil {
    return err
}

// Multiple error checks
result, err := SomeFunction()
if err != nil {
    return fmt.Errorf("failed to process: %w", err)
}

data, err := AnotherFunction(result)
if err != nil {
    return fmt.Errorf("failed to handle result: %w", err)
}

Advanced Error Handling

1. Error Wrapping

Go 1.13+ error wrapping:

// Wrapping errors
func ProcessFile(path string) error {
    data, err := readFile(path)
    if err != nil {
        return fmt.Errorf("failed to read file %s: %w", path, err)
    }
    
    if err := processData(data); err != nil {
        return fmt.Errorf("failed to process data: %w", err)
    }
    
    return nil
}

// Unwrapping errors
if errors.Is(err, fs.ErrNotExist) {
    // Handle file not found
}

var validationErr *ValidationError
if errors.As(err, &validationErr) {
    // Handle validation error
}

2. Custom Error Types

Rich error types:

type BusinessError struct {
    Code    string
    Message string
    Details map[string]interface{}
    err     error
}

func (e *BusinessError) Error() string {
    if e.err != nil {
        return fmt.Sprintf("%s: %s (%v)", e.Code, e.Message, e.err)
    }
    return fmt.Sprintf("%s: %s", e.Code, e.Message)
}

func (e *BusinessError) Unwrap() error {
    return e.err
}

// Usage
func ProcessOrder(order Order) error {
    if err := validateOrder(order); err != nil {
        return &BusinessError{
            Code:    "INVALID_ORDER",
            Message: "Order validation failed",
            Details: map[string]interface{}{
                "order_id": order.ID,
            },
            err: err,
        }
    }
    return nil
}

Best Practices

1. Error Context

Provide meaningful error context:

// Bad: insufficient context
return errors.New("validation failed")

// Good: rich context
return fmt.Errorf("order %s validation failed: invalid status %s", 
    order.ID, order.Status)

// Better: structured error
return &ValidationError{
    Field:   "status",
    OrderID: order.ID,
    Value:   order.Status,
    Message: "invalid order status",
}

2. Error Handling Patterns

Common patterns for different scenarios:

// Sentinel errors
var (
    ErrNotFound = errors.New("not found")
    ErrInvalid  = errors.New("invalid input")
)

// Error types
type NotFoundError struct {
    Resource string
    ID       string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s with ID %s not found", e.Resource, e.ID)
}

// Error handling middleware
func ErrorHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic: %v", err)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

3. Error Logging

Proper error logging practices:

// Bad: logging and returning
if err != nil {
    log.Printf("failed to process: %v", err)
    return err
}

// Good: log at the top level
func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}

func run() error {
    // Return errors without logging
    if err := process(); err != nil {
        return fmt.Errorf("failed to run: %w", err)
    }
    return nil
}

Common Patterns

1. Retry Pattern

Handle transient errors:

func WithRetry(fn func() error, attempts int, delay time.Duration) error {
    var err error
    for i := 0; i < attempts; i++ {
        err = fn()
        if err == nil {
            return nil
        }
        if i < attempts-1 {
            time.Sleep(delay)
        }
    }
    return fmt.Errorf("failed after %d attempts: %w", attempts, err)
}

// Usage
err := WithRetry(func() error {
    return callExternalService()
}, 3, time.Second)

2. Circuit Breaker

Prevent cascading failures:

type CircuitBreaker struct {
    failures    int64
    threshold   int64
    resetAfter  time.Duration
    lastFailure time.Time
}

func (cb *CircuitBreaker) Execute(fn func() error) error {
    if cb.isOpen() {
        return errors.New("circuit breaker is open")
    }
    
    err := fn()
    if err != nil {
        cb.recordFailure()
        return err
    }
    
    cb.reset()
    return nil
}

Error Handling in Tests

1. Testing Error Conditions

func TestProcess(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        wantErr bool
        errType error
    }{
        {
            name:    "valid input",
            input:   "valid",
            wantErr: false,
        },
        {
            name:    "invalid input",
            input:   "",
            wantErr: true,
            errType: ErrInvalid,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := Process(tt.input)
            if (err != nil) != tt.wantErr {
                t.Errorf("Process() error = %v, wantErr %v", err, tt.wantErr)
            }
            if tt.wantErr && !errors.Is(err, tt.errType) {
                t.Errorf("Process() error = %v, want %v", err, tt.errType)
            }
        })
    }
}

Next Steps