5.3 Error Wrapping (Go 1.13+)

Error wrapping allows you to add context to errors while preserving the original error. This is essential for building error chains that can be inspected and handled appropriately.

Using fmt.Errorf with %w

The %w verb in fmt.Errorf wraps an error:

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

Error wrapping serves multiple important purposes in Go programs. By adding context about where an error occurred, wrapping transforms a generic "file not found" error into "failed to read config: file not found," immediately clarifying the operation that failed. This context becomes invaluable when debugging, as error messages tell a story about the call chain that led to the failure.

Wrapping also preserves the original error within the chain, allowing you to inspect the full error history. This preservation enables sophisticated error handling: you can check for specific error types anywhere in the chain using errors.Is and errors.As, making it possible to handle particular error conditions even when they've been wrapped multiple times. This capability is essential for building robust error handling that can distinguish between different failure modes while maintaining rich diagnostic information.

Unwrapping Errors

Use errors.Unwrap to get the original error:

import "errors"

originalErr := errors.Unwrap(wrappedErr)

Checking Error Types

errors.Is: Checks if an error or any error in its chain matches a target:

if errors.Is(err, targetErr) {
    // err or any error in its chain matches targetErr
}

errors.As: Checks if an error or any error in its chain is of a specific type:

var targetErr *MyError
if errors.As(err, &targetErr) {
    // err or any error in its chain is of type *MyError
    // targetErr now contains the matched error
}

Complete example

package main

import (
    "errors"
    "fmt"
)

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

func findUser(id int) (string, error) {
    if id < 0 {
        return "", fmt.Errorf("findUser: %w", ErrInvalidInput)
    }
    if id == 0 {
        return "", fmt.Errorf("findUser: user %w", ErrNotFound)
    }
    return fmt.Sprintf("user-%d", id), nil
}

func processUser(id int) error {
    name, err := findUser(id)
    if err != nil {
        return fmt.Errorf("processUser: %w", err)
    }
    fmt.Println("Processing:", name)
    return nil
}

func main() {
    err := processUser(-1)
    if err != nil {
        fmt.Println("Error:", err)

        // Check if error chain contains ErrInvalidInput
        if errors.Is(err, ErrInvalidInput) {
            fmt.Println("Invalid input detected")
        }
    }
}

Sentinel Errors

Sentinel errors are predefined error values that represent specific error conditions. They're called "sentinels" because they act as markers that you can check for using errors.Is, even when the error has been wrapped multiple times.

The key characteristic of sentinel errors is that they're typically package-level variables defined using errors.New:

var ErrNotFound = errors.New("not found")
var ErrInvalidInput = errors.New("invalid input")
var ErrPermissionDenied = errors.New("permission denied")

Sentinel errors are useful because they allow you to check for specific error conditions without needing to know the exact error message or type. When you use errors.Is(err, ErrNotFound), it will find ErrNotFound anywhere in the error chain, even if it's been wrapped with additional context.

Best practices for sentinel errors

  1. Use descriptive names: Start with Err prefix (e.g., ErrNotFound, ErrInvalidInput)
  2. Make them exported: Use capital letters so callers can check for them
  3. Use errors.New: Create them once as package-level variables
  4. Document their meaning: Add comments explaining when they're returned
  5. Wrap them with context: Use fmt.Errorf with %w to add context while preserving the sentinel
package main

import (
    "errors"
    "fmt"
)

// Sentinel errors - predefined error values
var (
    ErrNotFound      = errors.New("not found")
    ErrInvalidInput  = errors.New("invalid input")
    ErrUnauthorized = errors.New("unauthorized")
)

func findUser(id int) (string, error) {
    if id < 0 {
        // Wrap sentinel error with context
        return "", fmt.Errorf("findUser: invalid id %d: %w", id, ErrInvalidInput)
    }
    if id == 0 {
        return "", fmt.Errorf("findUser: user %w", ErrNotFound)
    }
    return fmt.Sprintf("user-%d", id), nil
}

func main() {
    _, err := findUser(-1)
    if err != nil {
        // Check for sentinel error even though it's wrapped
        if errors.Is(err, ErrInvalidInput) {
            fmt.Println("Handling invalid input error")
        }
    }
}

Sentinel errors work particularly well with errors.Is because they can be checked for anywhere in an error chain. This makes them ideal for representing common error conditions that multiple functions might encounter.

Aggregating Multiple Errors (Go 1.20+)

Sometimes you need to collect and return multiple errors together. For example, when validating multiple fields, you might want to report all validation errors, not just the first one. Go 1.20 introduced errors.Join for this purpose.

errors.Join combines multiple errors into a single error. When you print the joined error, it shows all the individual errors. You can also use errors.Is and errors.As with joined errors to check for specific errors within the collection.

package main

import (
    "errors"
    "fmt"
)

func validateUser(name, email string) error {
    var errs []error

    if name == "" {
        errs = append(errs, errors.New("name is required"))
    }
    if email == "" {
        errs = append(errs, errors.New("email is required"))
    }
    if len(email) > 0 {
        hasAt := false
        for _, char := range email {
            if char == '@' {
                hasAt = true
                break
            }
        }
        if !hasAt {
            errs = append(errs, errors.New("email must contain @"))
        }
    }

    if len(errs) > 0 {
        return errors.Join(errs...)
    }
    return nil
}

func main() {
    err := validateUser("", "invalid-email")
    if err != nil {
        fmt.Println("Validation errors:")
        fmt.Println(err)
        // Output:
        // Validation errors:
        // name is required
        // email must contain @

        // Check for specific errors in the collection
        if errors.Is(err, errors.New("name is required")) {
            fmt.Println("Name validation failed")
        }
    }
}

errors.Join is particularly useful in scenarios where you want to collect all errors before returning, such as: