Error Handling

The error interface, creating errors, wrapping, errors.Is/As, and idiomatic patterns.

Intermediate 35 min read 🐹 Go

The error Interface

Go doesn't have exceptions. Instead, functions return an error value as the last return value. The caller checks if err != nil and handles the error explicitly. This makes error handling visible and forces you to think about failures.

import (
    "errors"
    "fmt"
    "os"
)

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

func main() {
    data, err := readConfig("config.json")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Config loaded:", len(data), "bytes")
}
Key Takeaway: Always check errors immediately. The pattern is: call, check, handle. Never ignore errors with _ unless you have a very good reason and document it.

Error Wrapping with %w

fmt.Errorf with %w wraps errors, preserving the original cause while adding context. errors.Is and errors.As unwrap to check the original error:

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

func findUser(id int) (*User, error) {
    // ... database query ...
    return nil, fmt.Errorf("findUser(%d): %w", id, ErrNotFound)
}

_, err := findUser(42)

// errors.Is checks the error chain
if errors.Is(err, ErrNotFound) {
    fmt.Println("User not found") // This matches!
}

// errors.As extracts a specific error type
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println("Path:", pathErr.Path)
}

Custom Error Types

type ValidationError struct {
    Field   string
    Message string
}

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

func validateAge(age int) error {
    if age < 0 || age > 150 {
        return &ValidationError{Field: "age", Message: "must be 0-150"}
    }
    return nil
}

err := validateAge(-5)
var valErr *ValidationError
if errors.As(err, &valErr) {
    fmt.Printf("Field: %s, Message: %s\n", valErr.Field, valErr.Message)
}

⚠️ Common Mistake: Comparing errors with ==

Use errors.Is(err, target) instead of err == target. errors.Is unwraps the error chain, so it works with wrapped errors. == only matches the exact error instance.

Error Handling Flow
Call func
returns (val, err)
if err != nil
check
Handle
wrap or return
Continue
use val

Practice Exercises

Medium Build a Mini Project

Combine concepts from this tutorial to build a small utility or tool.

Medium Debug Challenge

Introduce a bug in one of the code examples and practice finding and fixing it.

Hard Refactoring Exercise

Rewrite one example using a different approach and compare the tradeoffs.