Loading...
Loading...

Go Error Handling Tutorial

Error handling in Go is explicit and type-safe, following the language's philosophy of clear and predictable code. Unlike exception-based approaches, Go treats errors as normal return values, encouraging developers to handle them immediately and appropriately.

1. Basic Error Handling

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

type error interface {
    Error() string
}

1.1 Creating Simple Errors

The standard library provides several ways to create basic errors:

// Using errors.New()
import "errors"

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

// Using fmt.Errorf() for formatted errors
func login(username, password string) error {
    if username == "" {
        return fmt.Errorf("username cannot be empty")
    }
    if len(password) < 8 {
        return fmt.Errorf("password must be at least 8 characters, got %d", len(password))
    }
    return nil
}

1.2 Handling Errors

The idiomatic way to handle errors in Go is to check them immediately after the function call:

result, err := divide(10, 2)
if err != nil {
    fmt.Println("Error:", err)
    // Handle the error (return, retry, log, etc.)
    return
}
fmt.Println("Result:", result)

// Handling without using the value
if err := login("user", "pass"); err != nil {
    fmt.Println("Login failed:", err)
}

2. Custom Error Types

For more sophisticated error handling, you can create custom error types.

2.1 Struct-Based Errors

Define custom error types by implementing the error interface:

type TimeoutError struct {
    Operation string
    Duration  time.Duration
}

func (e *TimeoutError) Error() string {
    return fmt.Sprintf("%s timed out after %v", e.Operation, e.Duration)
}

func fetchData() error {
    // Simulate timeout
    return &TimeoutError{
        Operation: "database query",
        Duration:  5 * time.Second,
    }
}

// Usage
err := fetchData()
if err != nil {
    fmt.Println(err) // "database query timed out after 5s"
}

2.2 Type Assertions for Special Handling

Check for specific error types using type assertions:

err := fetchData()
if timeoutErr, ok := err.(*TimeoutError); ok {
    fmt.Printf("Timeout occurred during %s\n", timeoutErr.Operation)
    // Implement retry logic or other recovery
} else if err != nil {
    // Handle other errors
    fmt.Println("Error:", err)
}

3. Error Wrapping and Inspection

Go 1.13 introduced error wrapping features for building error chains.

3.1 Wrapping Errors

Use fmt.Errorf with %w verb to wrap errors:

import "os"

func readConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("failed to read config at %s: %w", path, err)
    }
    return data, nil
}

// Usage
_, err := readConfig("/nonexistent/config.json")
if err != nil {
    fmt.Println(err) // "failed to read config at /nonexistent/config.json: open /nonexistent/config.json: no such file or directory"
}

3.2 Inspecting Error Chains

Use errors.Is and errors.As to examine wrapped errors:

// Checking for specific error values
if errors.Is(err, os.ErrNotExist) {
    fmt.Println("Config file doesn't exist")
}

// Extracting specific error types
var pathError *os.PathError
if errors.As(err, &pathError) {
    fmt.Printf("Operation: %s, Path: %s\n", pathError.Op, pathError.Path)
}

4. Advanced Error Handling Patterns

4.1 Sentinel Errors

Predefined error values for specific error conditions:

var (
    ErrUnauthorized = errors.New("unauthorized access")
    ErrInvalidInput = errors.New("invalid input")
)

func authenticate(token string) error {
    if token == "" {
        return ErrUnauthorized
    }
    // Authentication logic
    return nil
}

// Usage
err := authenticate("")
if errors.Is(err, ErrUnauthorized) {
    fmt.Println("Please log in first")
}

4.2 Error Aggregation

Combining multiple errors into a single error:

func validateUser(user User) error {
    var errs []error
    
    if user.Name == "" {
        errs = append(errs, errors.New("name is required"))
    }
    if user.Age < 18 {
        errs = append(errs, errors.New("age must be 18+"))
    }
    
    if len(errs) > 0 {
        return fmt.Errorf("validation failed: %v", errs)
    }
    return nil
}

5. Context and Error Handling

5.1 Errors with Context

Adding contextual information to errors:

type ContextError struct {
    Context string
    Err     error
}

func (e *ContextError) Error() string {
    return fmt.Sprintf("%s: %v", e.Context, e.Err)
}

func (e *ContextError) Unwrap() error {
    return e.Err
}

func processFile(filename string) error {
    data, err := os.ReadFile(filename)
    if err != nil {
        return &ContextError{
            Context: fmt.Sprintf("processing %s", filename),
            Err:     err,
        }
    }
    // Process data
    return nil
}

5.2 Timeout and Cancellation

Handling context cancellation in errors:

func longOperation(ctx context.Context) error {
    select {
    case <-time.After(5 * time.Second):
        return nil // Operation completed
    case <-ctx.Done():
        return ctx.Err() // Returns context.Canceled or context.DeadlineExceeded
    }
}

// Usage
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

err := longOperation(ctx)
if errors.Is(err, context.DeadlineExceeded) {
    fmt.Println("Operation timed out")
}

6. Error Handling Best Practices

6.1 When to Handle vs When to Return

Guidelines for error propagation:

// Good: Handle errors you can recover from
func parseNumber(s string) (int, error) {
    n, err := strconv.Atoi(s)
    if err != nil {
        // Can't recover from this - return the error
        return 0, fmt.Errorf("invalid number: %w", err)
    }
    return n, nil
}

// Good: Add context when returning errors
func loadUser(id string) (*User, error) {
    data, err := db.Query("SELECT * FROM users WHERE id = ?", id)
    if err != nil {
        return nil, fmt.Errorf("loading user %s: %w", id, err)
    }
    // ...
}

// Bad: Don't ignore errors
file, _ := os.Open("file.txt") // WRONG - always handle errors

6.2 Logging Strategies

Effective error logging patterns:

// Good: Log with context
if err := processOrder(order); err != nil {
    log.Printf("Failed to process order %s: %v", order.ID, err)
    // Return a cleaned error to the caller
    return fmt.Errorf("order processing failed")
}

// Good: Structured logging
if err := sendEmail(user); err != nil {
    log.WithFields(log.Fields{
        "user":  user.ID,
        "email": user.Email,
    }).Error("Failed to send email")
}

7. Recovery and Panics

7.1 Handling Panics

Recovering from unexpected errors:

func safeOperation() (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()
    
    // Potentially panicking code
    return nil
}

// Usage
err := safeOperation()
if err != nil {
    fmt.Println("Operation failed:", err)
}

7.2 When to Use Panic

Appropriate uses of panic/recover:

// Appropriate: Programmer errors during development
func mustParse(s string) int {
    n, err := strconv.Atoi(s)
    if err != nil {
        panic(fmt.Sprintf("invalid number in mustParse: %s", s))
    }
    return n
}

// Appropriate: Initialization failures
func init() {
    if os.Getenv("DB_URL") == "" {
        panic("DB_URL environment variable not set")
    }
}

// Inappropriate: Don't use panic for normal error conditions
func getUser(id string) (User, error) {
    // Return error instead of panic when user not found
}
0 Interaction
0 Views
Views
0 Likes
×
×
×
🍪 CookieConsent@Ptutorials:~

Welcome to Ptutorials

$ Allow cookies on this site ? (y/n)

top-home