Go Error Handling

The error Interface

Errors are values, not exceptions
// The error interface
type error interface {
    Error() string
}

// Check errors immediately after the call
f, err := os.Open("/etc/hosts")
if err != nil {
    log.Fatal(err)
}
defer f.Close()

Go has no try/catch. Errors are returned as the last value. The caller decides what to do.

Creating Errors

errors.New and fmt.Errorf
// Sentinel error — package-level variable
var ErrNotFound = errors.New("not found")

// Formatted error with context
func findHost(name string) (*Host, error) {
    return nil, fmt.Errorf("host %q not found in inventory", name)
}

Wrapping Errors

%w wraps an error, preserving the chain
func readConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("readConfig: %w", err)
    }
    return data, nil
}

// Caller unwraps to find root cause
if errors.Is(err, os.ErrNotExist) {
    fmt.Println("config file missing")
}

%w (not %v) wraps the error so errors.Is and errors.As can traverse the chain.

errors.Is and errors.As

Check identity and extract typed errors
// errors.Is — does any error in the chain match?
if errors.Is(err, os.ErrPermission) {
    fmt.Println("permission denied")
}

// errors.As — extract a specific error type
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Printf("op %s on %s: %v\n", pathErr.Op, pathErr.Path, pathErr.Err)
}

errors.Is checks for identity (sentinel errors). errors.As checks for type and extracts the value. Both traverse the full wrap chain.

Custom Error Types

Implement error for structured errors
type ValidationError struct {
    Field   string
    Message string
}

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

func validatePort(port int) error {
    if port < 1 || port > 65535 {
        return &ValidationError{
            Field:   "port",
            Message: fmt.Sprintf("%d out of range [1-65535]", port),
        }
    }
    return nil
}

Panic and Recover

panic for unrecoverable bugs, recover at API boundaries
// panic — programmer errors only
func MustParse(s string) int {
    n, err := strconv.Atoi(s)
    if err != nil {
        panic(fmt.Sprintf("MustParse(%q): %v", s, err))
    }
    return n
}

// recover — catch panics at boundaries
func safeHandler(w http.ResponseWriter, r *http.Request) {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("panic recovered: %v", r)
            http.Error(w, "internal error", 500)
        }
    }()
    // handler code
}

panic is for bugs, not expected failures. Return error for everything else.

Idiomatic Pattern

Happy path down the left margin
func processFile(path string) error {
    data, err := os.ReadFile(path)
    if err != nil {
        return fmt.Errorf("read: %w", err)
    }

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

    if err := save(result); err != nil {
        return fmt.Errorf("save: %w", err)
    }

    return nil
}

Each error check adds context and returns immediately. The happy path flows straight down the left margin.