Go (Golang) Tutorial 0/45 lessons ~6 min read Lesson 11

    Error Handling Basics

    Go treats errors as values implementing the error interface: Error() string.

    Course progress0%
    Focus
    10 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Go treats errors as values implementing the error interface: Error() string. There are no exceptions for control flow — panics are reserved for truly unrecoverable programmer errors. Production services build error chains with fmt.Errorf and %w, inspect them with errors.Is and errors.As, and define sentinel errors with errors.New.

    Understanding when to wrap vs return raw errors, how to create custom error types with additional fields, and defer+recover for graceful shutdown separates junior from mid-level Go developers in interviews.

    This lesson covers the error handling patterns used in Kubernetes, etcd, and every major Go open-source project — explicit, verbose, but predictable and debuggable.

    The story

    When a Kubernetes controller fails to patch a CustomResource, it wraps the API error with context: fmt.Errorf("reconcile deployment %s: %w", name, err). Operators use errors.Is to detect context.DeadlineExceeded and retry, or errors.As to extract structured API status details — the same patterns in client-go that every platform engineer reads during incident response.

    Explicit error handling is verbose but predictable: no hidden stack unwinding across goroutine boundaries in a payment service processing millions of transactions.

    Understanding the topic

    Key concepts

    • error interface: type with Error() string method.
    • errors.New and fmt.Errorf create errors; %w wraps for errors.Is/As.
    • Sentinel errors: var ErrNotFound = errors.New("not found").
    • Custom errors: type MyError struct { Code int; Msg string }.
    • errors.Is(err, target) checks chain; errors.As(err, &target) extracts type.
    • panic/recover — recover only useful inside defer; don't use for normal flow.
    text
    flowchart TD
    Call[Function Call] --> Err{error != nil?}
    Err -->|yes| Handle[Handle / Wrap]
    Err -->|no| Success[Use Result]
    Handle --> Propagate[Return Up]

    Step-by-step explanation

    1. Function returns error as last value; caller checks if err != nil.
    2. Wrap: fmt.Errorf("fetch user %s: %w", id, err) preserves chain.
    3. Compare: errors.Is(err, ErrNotFound) walks unwrap chain.
    4. Extract: var e *MyError; errors.As(err, &e) gets typed error.
    5. panic stops normal execution; recover in defer catches it.
    6. log.Fatal calls os.Exit — use only in main, not libraries.

    Practical code example

    Sentinel errors, wrapping, and errors.Is/As inspection:

    go
    package main
    import (
    "errors"
    "fmt"
    )
    var ErrNotFound = errors.New("not found")
    type ValidationError struct {
    Field string
    Reason string
    }
    func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation: %s — %s", e.Field, e.Reason)
    }
    func findUser(id string) error {
    if id == "" {
    return &ValidationError{Field: "id", Reason: "required"}
    }
    if id == "missing" {
    return fmt.Errorf("db lookup %s: %w", id, ErrNotFound)
    }
    return nil
    }
    func main() {
    err := findUser("missing")
    if errors.Is(err, ErrNotFound) {
    fmt.Println("handled: user not found")
    }
    var ve *ValidationError
    if errors.As(findUser(""), &ve) {
    fmt.Printf("validation failed on %s\n", ve.Field)
    }
    }

    Line-by-line code explanation

    • if err != nil is the mandatory check after almost every fallible operation.
    • fmt.Errorf("context: %w", err) wraps an error while preserving the chain for errors.Is.
    • errors.Is(err, context.DeadlineExceeded) checks whether any wrapped error matches a sentinel.
    • errors.As(err, &apiErr) extracts a typed error (e.g., *APIStatusError) from the chain.
    • var ErrNotFound = errors.New("not found") defines a package-level sentinel for domain errors.
    • return fmt.Errorf("fetch zone %s: %w", id, err) adds human context without losing the root cause.
    • errors.Join(err1, err2) (Go 1.20+) combines multiple errors from parallel operations.
    • log.Printf("warning: %v", err) logs the outermost message; use %+v with pkg/errors for stacks.

    Key takeaway: %w enables errors.Is/As. Custom error types carry structured data for API responses. Never panic in libraries.

    Real-world use

    Where you'll use this in production

    • HTTP handlers mapping ErrNotFound to 404, validation to 400.
    • Database layer translating sql.ErrNoRows to domain errors.
    • Retry logic checking errors.Is(err, context.DeadlineExceeded).
    • Structured logging with error chains in distributed tracing.

    Best practices

    • Add context when wrapping: operation + identifier + %w.
    • Export sentinel errors for API consumers to check with errors.Is.
    • Return error, never panic, from library functions.
    • Use fmt.Errorf with %w, not %v, when caller needs Is/As.
    • Handle errors once — wrap upward or log at top, not both repeatedly.
    • Document exported error variables in godoc.

    Common mistakes

    • Comparing err == ErrNotFound when err is wrapped — use errors.Is.
    • Using panic for validation errors in HTTP handlers.
    • Swallowing errors with _ — silent failures in production.
    • Wrapping with %v instead of %w — breaks error chain.
    • log.Fatal in reusable packages — prevents caller cleanup.

    Advanced interview questions

    Q1BeginnerGo error interface?
    type error interface { Error() string } — any type implementing it is an error.
    Q2Beginnerpanic vs error return?
    Errors for expected failures; panic for unrecoverable bugs — handlers shouldn't panic on bad input.
    Q3IntermediatePurpose of %w in fmt.Errorf?
    Wraps error preserving chain for errors.Is and errors.As inspection.
    Q4Intermediateerrors.Is vs errors.As?
    Is checks sentinel equality in chain; As extracts typed error into target pointer.
    Q5AdvancedDesign error handling for REST API with domain errors.
    Sentinel + typed errors in domain; handler maps to HTTP status; middleware logs with request ID; never expose internal details in JSON.

    Summary

    Errors are values; check if err != nil after every fallible call. Wrap with fmt.Errorf and %w; inspect with errors.Is/As. Sentinel and custom typed errors enable structured handling. panic/recover only for truly exceptional cases, not control flow. Next lesson: arrays — fixed-size collections.

    Ready to mark this lesson complete?Track your journey across the entire course.