05 — Errors

How Go reports and inspects failures: errors as plain values, sentinel errors, wrapping with context, and the errors.Is / errors.As helpers. Every example is live — edit it and press Run.

1. Errors are values; wrap with %w

Go has no exceptions for ordinary failures. A function returns an error as its last result and the caller checks it; error is just an interface with an Error() string method. A sentinel is a known error value you can test for, and fmt.Errorf with the %w verb wraps it — adding context while keeping the original reachable. errors.Is then finds it anywhere in the chain:

package main

import (
    "errors"
    "fmt"
)

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

func Lookup(m map[string]int, key string) (int, error) {
    if v, ok := m[key]; ok {
        return v, nil
    }
    return 0, fmt.Errorf("lookup %s: %w", key, ErrNotFound) // wrap with context
}

func main() {
    m := map[string]int{"a": 1}
    _, err := Lookup(m, "z")
    fmt.Println(err)
    fmt.Println(errors.Is(err, ErrNotFound)) // true, despite the wrapping
}

Output:

lookup z: not found
true

Use %w (not %v) when you want callers to still detect the wrapped error — %v would stringify it and throw the link away.

2. Custom error types and errors.As

When you need structured detail — which field failed, an error code — define a type with an Error() method. errors.As searches the chain for an error of a given type and fills in your variable, so you can read its fields even through wrapping. It's the checked way to recover a concrete error type:

package main

import (
    "errors"
    "fmt"
)

type ParseError struct {
    Field string
    Msg   string
}

func (e *ParseError) Error() string {
    return e.Field + ": " + e.Msg
}

func Validate(name string, age int) error {
    if name == "" {
        return &ParseError{Field: "name", Msg: "empty"}
    }
    if age < 0 {
        return &ParseError{Field: "age", Msg: "negative"}
    }
    return nil
}

func main() {
    err := fmt.Errorf("outer: %w", Validate("", 5)) // wrapped
    fmt.Println(err)

    var pe *ParseError
    if errors.As(err, &pe) {        // find the *ParseError in the chain
        fmt.Println("field:", pe.Field)
    }
}

Output:

outer: name: empty
field: name

You pass the address of the pointer (&pe): errors.As sets it to the found error. Note the method has a pointer receiver, so it's *ParseError that satisfies error — which is why you build &ParseError{...}.

Recap

ConceptIn Go
Error typeerror interface: Error() string
Report failureReturn (result, error); nil means success
SentinelPackage-level var ErrX = errors.New(...)
Wrapfmt.Errorf("ctx: %w", err) — keeps the cause
Match a sentinelerrors.Is(err, ErrX)
Recover a typeerrors.As(err, &target)

Next: 06 — Goroutines.