Free — no signup required

Error Handling Patterns

2 min read

Errors Are Values, Not Exceptions

Go has no try/catch. There are no exceptions that bubble up the call stack invisibly. Instead, errors are ordinary values — functions that can fail return an error as their last return value, and the caller checks it immediately.

file, err := os.Open("/etc/hosts")
if err != nil {
    log.Fatalf("Cannot open hosts file: %v", err)
}
defer file.Close()
// Safe to use file here

This pattern forces you to think about failure at the point where it can occur, not somewhere up the call stack where context has been lost. The result is code where every failure path is explicit and visible.

Wrapping errors with context

When you return an error from a function, add context about what you were trying to do. Use fmt.Errorf with the %w verb to wrap the original error — this preserves the original error for programmatic inspection while adding a human-readable description.

func loadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        // Wrap: adds "load config /etc/myapp/config.yaml:" prefix
        return nil, fmt.Errorf("load config %s: %w", path, err)
    }

    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return nil, fmt.Errorf("parse config %s: %w", path, err)
    }

    return &cfg, nil
}

When this error reaches the top of your program, it reads like a breadcrumb trail:

load config /etc/myapp/config.yaml: open /etc/myapp/config.yaml: no such file or directory

Inspecting wrapped errors

Use errors.Is to check if an error (or any error in its chain) matches a specific sentinel error, and errors.As to extract a specific error type from the chain:

if errors.Is(err, os.ErrNotExist) {
    // File doesn't exist — create it with defaults
    return createDefaultConfig(path)
}

What NOT to do

// Bad: silently ignoring the error
result, _ := doSomething()

// Bad: using result before checking error
result, err := doSomething()
fmt.Println(result) // result may be zero value if err != nil
if err != nil { ... }

Ignoring errors with _ is occasionally valid (e.g., closing a read-only file where the close error is irrelevant), but it should be a conscious, documented decision — not a habit.

Interview Tip

Interviewers often ask: "How does Go's error handling differ from exception-based languages, and what are the trade-offs?" The answer: Go errors are explicit values returned alongside results, forcing callers to handle failures at the call site. This makes error paths visible in code review and prevents silent failures. The trade-off is verbosity — if err != nil appears frequently. The %w wrapping pattern and errors.Is/errors.As for inspection are the idiomatic solutions for building error chains that are both human-readable and machine-inspectable.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free