1. go
  2. /basics
  3. /error-handling

Complete Guide to Error Handling in Go

Error handling is a critical aspect of Go programming. This guide covers everything you need to know about working with errors effectively in Go.

Understanding Errors

In Go, errors are values that implement the error interface:

type error interface {
    Error() string
}

Basic Error Handling

// Function that returns an error
func divide(x, y float64) (float64, error) {
    if y == 0 {
        return 0, errors.New("division by zero")
    }
    return x / y, nil
}

// Using the function
result, err := divide(10, 0)
if err != nil {
    log.Printf("Error: %v", err)
    return
}
fmt.Printf("Result: %f\n", result)

Creating Errors

Using errors.New

// Simple error creation
err := errors.New("something went wrong")

// With formatting
err = fmt.Errorf("processing failed: %v", underlying)

Custom Error Types

// Define custom error type
type ValidationError struct {
    Field string
    Message string
}

// Implement error interface
func (v *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", v.Field, v.Message)
}

// Using custom error
func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{
            Field: "age",
            Message: "must be positive",
        }
    }
    return nil
}

// Handling custom error
if err := validateAge(-5); err != nil {
    if ve, ok := err.(*ValidationError); ok {
        fmt.Printf("Validation error on field %s: %s\n", 
            ve.Field, ve.Message)
    } else {
        fmt.Printf("Other error: %v\n", err)
    }
}

Error Wrapping

Using fmt.Errorf with %w

func processFile(path string) error {
    file, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("opening file %s: %w", path, err)
    }
    defer file.Close()

    if err := processContent(file); err != nil {
        return fmt.Errorf("processing content: %w", err)
    }

    return nil
}

Unwrapping Errors

// Check if error contains specific error
if errors.Is(err, os.ErrNotExist) {
    // Handle file not found
}

// Get underlying error of specific type
var pathError *os.PathError
if errors.As(err, &pathError) {
    fmt.Printf("Path error: %v\n", pathError.Path)
}

Error Handling Patterns

Sentinel Errors

// Define sentinel errors
var (
    ErrNotFound = errors.New("not found")
    ErrInvalidInput = errors.New("invalid input")
)

func findUser(id string) (*User, error) {
    if id == "" {
        return nil, ErrInvalidInput
    }
    
    user := database.Find(id)
    if user == nil {
        return nil, ErrNotFound
    }
    
    return user, nil
}

// Using sentinel errors
user, err := findUser(id)
switch {
case errors.Is(err, ErrNotFound):
    // Handle not found
case errors.Is(err, ErrInvalidInput):
    // Handle invalid input
case err != nil:
    // Handle other errors
default:
    // Process user
}

Error Types

// Define error types for different categories
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)
}

type ValidationError struct {
    Field string
    Reason string
}

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

// Using error types
func processUser(id string) error {
    user, err := findUser(id)
    if err != nil {
        return &NotFoundError{
            Resource: "user",
            ID:      id,
        }
    }
    
    if err := validateUser(user); err != nil {
        return fmt.Errorf("validation failed: %w", err)
    }
    
    return nil
}

Error Handling Best Practices

1. Don't Ignore Errors

// Bad: Ignoring errors
file.Close()

// Good: Handle the error
if err := file.Close(); err != nil {
    log.Printf("error closing file: %v", err)
}

2. Add Context to Errors

// Bad: Returning raw error
if err := doSomething(); err != nil {
    return err
}

// Good: Add context
if err := doSomething(); err != nil {
    return fmt.Errorf("processing request: %w", err)
}

3. Handle Special Cases

func readConfig(path string) (*Config, error) {
    data, err := ioutil.ReadFile(path)
    if err != nil {
        if errors.Is(err, os.ErrNotExist) {
            // Return default config if file doesn't exist
            return DefaultConfig(), nil
        }
        return nil, fmt.Errorf("reading config: %w", err)
    }
    
    var config Config
    if err := json.Unmarshal(data, &config); err != nil {
        return nil, fmt.Errorf("parsing config: %w", err)
    }
    
    return &config, nil
}

Advanced Error Handling

Error Groups

import "golang.org/x/sync/errgroup"

func processItems(items []Item) error {
    g := new(errgroup.Group)
    
    for _, item := range items {
        item := item // Create new variable for closure
        g.Go(func() error {
            return processItem(item)
        })
    }
    
    return g.Wait()
}

Custom Error Handling

type ErrorHandler interface {
    Handle(error)
}

type LogErrorHandler struct {
    logger *log.Logger
}

func (h *LogErrorHandler) Handle(err error) {
    h.logger.Printf("Error: %v", err)
}

func processWithHandler(handler ErrorHandler) {
    if err := doSomething(); err != nil {
        handler.Handle(err)
    }
}

Retry with Backoff

func retryWithBackoff(operation func() error) error {
    backoff := time.Second
    maxRetries := 3

    for i := 0; i < maxRetries; i++ {
        err := operation()
        if err == nil {
            return nil
        }

        if i == maxRetries-1 {
            return fmt.Errorf("operation failed after %d retries: %w", 
                maxRetries, err)
        }

        time.Sleep(backoff)
        backoff *= 2 // Exponential backoff
    }

    return nil
}

Testing Error Handling

func TestDivide(t *testing.T) {
    tests := []struct {
        name    string
        x, y    float64
        want    float64
        wantErr bool
    }{
        {"valid", 10, 2, 5, false},
        {"division by zero", 10, 0, 0, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := divide(tt.x, tt.y)
            if (err != nil) != tt.wantErr {
                t.Errorf("divide() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if err == nil && got != tt.want {
                t.Errorf("divide() = %v, want %v", got, tt.want)
            }
        })
    }
}

Common Error Handling Scenarios

1. Database Operations

func getUserByID(id string) (*User, error) {
    var user User
    err := db.QueryRow("SELECT * FROM users WHERE id = ?", id).
        Scan(&user.ID, &user.Name)
    
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, &NotFoundError{
                Resource: "user",
                ID:      id,
            }
        }
        return nil, fmt.Errorf("querying user: %w", err)
    }
    
    return &user, nil
}

2. HTTP Handlers

func handleUser(w http.ResponseWriter, r *http.Request) {
    user, err := getUserFromRequest(r)
    if err != nil {
        switch {
        case errors.Is(err, ErrInvalidInput):
            http.Error(w, err.Error(), http.StatusBadRequest)
        case errors.As(err, &NotFoundError{}):
            http.Error(w, err.Error(), http.StatusNotFound)
        default:
            http.Error(w, "Internal server error", 
                http.StatusInternalServerError)
        }
        return
    }
    
    // Process user...
}

Next Steps

  1. Learn about panic and recover
  2. Explore logging
  3. Study testing
  4. Practice with error handling patterns

Additional Resources