Skip to content

Table of Contents

Errors

Contents

The error interface

type error interface {
    Error() string
}

Errors are values. The convention: return error as the last return value; nil means success.

Creating errors

import "errors"

errors.New("something broke")
fmt.Errorf("user %d not found", id)          // formatted, no wrapping
fmt.Errorf("load config: %w", err)           // formatted, WRAPS err

Wrapping with %w

%w embeds an error so it can be unwrapped later. %v only stringifies (loses identity).

if err != nil {
    return fmt.Errorf("query users: %w", err) // preserves err for Is/As
}

// Multiple %w (Go 1.20+) wraps several errors:
return fmt.Errorf("op failed: %w and %w", err1, err2)

Manual unwrap:

errors.Unwrap(wrapped)      // returns the next error in the chain, or nil

errors.Is

Walks the wrap chain and reports whether any error in it matches a target (by == or a custom Is method). Use for sentinel comparison.

if errors.Is(err, sql.ErrNoRows) {
    // handle "not found" even if err is wrapped several layers deep
}
if errors.Is(err, os.ErrNotExist) { ... }

// Don't do this — breaks when wrapped:
if err == sql.ErrNoRows { ... }   // fragile

errors.As

Walks the chain and, if it finds an error assignable to target's type, assigns it. Use to extract a concrete/custom error type.

var pathErr *os.PathError
if errors.As(err, &pathErr) {     // note: &target
    fmt.Println(pathErr.Path, pathErr.Op)
}

var myErr *ValidationError
if errors.As(err, &myErr) {
    fmt.Println(myErr.Field)
}

target must be a non-nil pointer to either a type implementing error or an interface.

errors.Join

Combine multiple errors into one (Go 1.20+). The result's Error() lists each on its own line; Is/As check all of them. Nil args are skipped; all-nil returns nil.

var errs error
for _, f := range files {
    if err := process(f); err != nil {
        errs = errors.Join(errs, err)
    }
}
return errs                 // nil if everything succeeded

// Or all at once:
return errors.Join(err1, err2, err3)

Sentinel errors

Exported package-level error values for callers to compare against with errors.Is.

package store

var (
    ErrNotFound = errors.New("store: not found")
    ErrConflict = errors.New("store: conflict")
)

func (s *Store) Get(id string) (*Item, error) {
    // ...
    return nil, fmt.Errorf("get %s: %w", id, ErrNotFound)
}

// Caller:
if errors.Is(err, store.ErrNotFound) { ... }

Trade-off: sentinels create an API coupling. Prefer them for a small, stable set of conditions.

Custom error types

When callers need structured data, define a type implementing error.

type ValidationError struct {
    Field string
    Msg   string
}

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

// Support wrapping by implementing Unwrap:
type QueryError struct {
    Query string
    Err   error
}
func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }
func (e *QueryError) Unwrap() error { return e.Err }  // enables Is/As through it

// Custom Is for semantic matching:
func (e *ValidationError) Is(target error) bool {
    _, ok := target.(*ValidationError)
    return ok
}

// Usage:
return &ValidationError{Field: "email", Msg: "required"}

Use pointer receivers so errors.As(err, &target) matches a *ValidationError.

panic & recover

panic unwinds the stack running deferred funcs; recover (only useful inside a deferred func) stops the unwinding. Reserve panics for truly unrecoverable/programmer errors — not normal control flow.

func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered: %v", r)
        }
    }()
    return a / b, nil       // division by zero panics; recovered above
}

// Re-panic if it's not the case you handle:
defer func() {
    if r := recover(); r != nil {
        if e, ok := r.(net.Error); ok {
            handle(e)
        } else {
            panic(r)        // unknown: let it propagate
        }
    }
}()

panic("boom")               // panics with a string
panic(fmt.Errorf("bad: %w", err)) // panics with an error value

Notes:

  • A panic in a goroutine that isn't recovered crashes the whole program.
  • recover returns nil if there's no active panic.
  • Deferred funcs still run during a panic — that's how cleanup/recover works.

Idiomatic patterns

// 1. Handle errors immediately; don't shadow with :=
data, err := os.ReadFile(path)
if err != nil {
    return fmt.Errorf("read %s: %w", path, err) // add context, wrap
}

// 2. Add context as the error travels up; keep messages lowercase, no trailing punctuation.
//    Good:  "read config: open file: permission denied"

// 3. Sentinel check at the boundary you care about:
switch {
case errors.Is(err, ErrNotFound):
    http.Error(w, "not found", 404)
case err != nil:
    http.Error(w, "internal", 500)
}

// 4. Don't log AND return the same error (double reporting). Pick one layer to log.

// 5. Ignore errors explicitly when truly safe:
_ = resp.Body.Close()
defer func() { _ = f.Close() }()

// 6. Wrap once; let errors.Is/As traverse the chain.

// 7. For "must not fail" init code, panic is acceptable:
var tmpl = template.Must(template.ParseFiles("a.tmpl"))