Wrapping and Inspecting Errors

Overview

Go 1.13 introduced error wrapping, allowing you to add context while preserving the original error for inspection.

Wrapping Errors

originalErr := errors.New("file not found")

// %w wraps the error
wrappedErr := fmt.Errorf("loading config: %w", originalErr)

// The error chain: wrappedErr -> originalErr

Unwrapping

err := fmt.Errorf("outer: %w",
    fmt.Errorf("middle: %w",
        errors.New("inner")))

inner := errors.Unwrap(err)  // middle: inner

errors.Is

Check if any error in the chain matches:

var ErrNotFound = errors.New("not found")

err := fmt.Errorf("user lookup: %w", ErrNotFound)

if errors.Is(err, ErrNotFound) {
    // Handle not found
}

errors.As

Extract specific error types:

type ValidationError struct {
    Field string
}

func (e *ValidationError) Error() string {
    return "validation: " + e.Field
}

err := fmt.Errorf("processing: %w", &ValidationError{Field: "email"})

var valErr *ValidationError
if errors.As(err, &valErr) {
    fmt.Println("Invalid field:", valErr.Field)
}

Custom Wrappers

type WrappedError struct {
    Context string
    Err     error
}

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

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

Best Practices

// Add context when crossing boundaries
func LoadUser(id int) (*User, error) {
    data, err := db.Query(id)
    if err != nil {
        return nil, fmt.Errorf("LoadUser(%d): %w", id, err)
    }
    return parse(data)
}

Summary

Function Purpose
fmt.Errorf("%w", err) Wrap error
errors.Unwrap(err) Get wrapped error
errors.Is(err, target) Check chain for match
errors.As(err, &target) Extract typed error