Errors#

Errors in Go are values. There are no exceptions; functions that can fail return (value, error), and the caller checks the error. panic and recover exist for truly unrecoverable bugs (data races, out-of-bounds, programmer mistakes) but do not use them for normal control flow.

/controls`. For the errors package, see Libraries.

The error interface#

error is a single-method interface; any type implementing Error() string satisfies it.

type error interface {
    Error() string
}

The convention: return nil on success, a non-nil error on failure.

func load(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }
    return data, nil
}

if data, err := load("config.json"); err != nil {
    log.Fatal(err)
}

Wrapping#

fmt.Errorf with %w wraps an underlying error, adding context while preserving the wrapped value for later inspection.

func loadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("loadConfig %q: %w", path, err)
    }
    var c Config
    if err := json.Unmarshal(data, &c); err != nil {
        return nil, fmt.Errorf("loadConfig %q: %w", path, err)
    }
    return &c, nil
}

Wrap at every layer the error crosses, adding the caller’s context. The final message reads as a trace.

errors.Is and errors.As#

errors.Is(err, target) walks the wrap chain looking for equality with target. Useful for sentinel errors.

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

func get(id string) (*User, error) {
    /* … */
    return nil, ErrNotFound
}

_, err := get("missing")
if errors.Is(err, ErrNotFound) {
    // handle the not-found case
}

errors.As(err, &target) walks the chain and extracts an error of a specific concrete type.

var nerr *net.OpError
if errors.As(err, &nerr) {
    fmt.Println("network error on", nerr.Net, nerr.Addr)
}

Sentinel errors#

Package-level var ErrXxx = errors.New(...) declarations the caller compares against with errors.Is. The operator exports them when the caller plausibly wants to branch on them.

var (
    ErrTimeout    = errors.New("timeout")
    ErrUnauthorized = errors.New("unauthorized")
)

Typed errors#

For errors that carry structured data, the operator defines a type implementing Error().

type HTTPError struct {
    Status int
    Body   []byte
}

func (e *HTTPError) Error() string {
    return fmt.Sprintf("HTTP %d", e.Status)
}

// unwrap support
func (e *HTTPError) Unwrap() error { return nil }

_, err := call()
var herr *HTTPError
if errors.As(err, &herr) && herr.Status == 401 {
    refresh()
}

errors.Join#

Combine multiple errors into one (Go 1.20+). errors.Is and errors.As look at every joined member.

var joined error
for _, item := range items {
    if err := process(item); err != nil {
        joined = errors.Join(joined, fmt.Errorf("%s: %w", item, err))
    }
}
if joined != nil { return joined }

panic and recover#

panic(value) aborts the current goroutine; the stack unwinds, calling deferred functions on the way. recover() inside a deferred function catches the panic and turns it back into a value you can inspect.

func safeRun(f func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic: %v", r)
        }
    }()
    f()
    return nil
}

Use recover at process boundaries (HTTP handlers, RPC handlers, goroutine entry points) to keep a bug in one request from killing the whole binary. Inside normal code, panic is reserved for programmer errors.

must helpers#

For one-off setup that genuinely cannot fail, the operator wraps (T, error) calls in a must helper that panics on error.

func must[T any](v T, err error) T {
    if err != nil { panic(err) }
    return v
}

var tmpl = must(template.New("").Parse(rawTpl))

Use sparingly; the panic skips every cleanup defer below the call site.

References#