Errors and wrapping

Go · Practical · 14 min

Explicit error handling is the most visible Go idiom, and good wrapping makes failures debuggable.

In Go an error is just a value implementing interface { Error() string }. Functions that can fail return it last, and callers check it immediately:

f, err := os.Open(path)
if err != nil {
	return fmt.Errorf("load config: %w", err)
}

Handle each error once: either deal with it (retry, fallback, log and continue) or return it with added context. Don't log and return, or you'll see the same failure several times.

Creating errors:

  • errors.New("msg") for a fixed message. Exported package-level ones are sentinel errors, like io.EOF or var ErrNotFound = errors.New("not found").
  • fmt.Errorf("parse %q: %w", name, err) adds context and wraps the original. %v would only copy the text.
  • A custom type (e.g. type ValidationError struct{ Field string } with an Error method) when callers need structured details.

Inspecting errors — always through the chain of wrapped errors, never by comparing strings:

  • errors.Is(err, ErrNotFound) reports whether any error in the chain equals the target.
  • errors.As(err, &target) finds the first error in the chain assignable to target's type and stores it.
  • errors.Join(e1, e2) combines several errors; Is/As search all of them.

Error strings are lower-case with no trailing punctuation, since they get prefixed: open config: read file: permission denied. Wrapping with %w makes the wrapped error part of your API, so wrap deliberately.

Example

package main

import (
	"errors"
	"fmt"
)

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

type ValidationError struct{ Field string }

func (e *ValidationError) Error() string { return "invalid " + e.Field }

func find(id int) error {
	if id < 0 {
		return &ValidationError{Field: "id"}
	}
	return fmt.Errorf("find user %d: %w", id, ErrNotFound)
}

func main() {
	err := find(7)
	fmt.Println(err)                         // find user 7: not found
	fmt.Println(errors.Is(err, ErrNotFound)) // true

	var ve *ValidationError
	if errors.As(find(-1), &ve) {
		fmt.Println("bad field:", ve.Field) // bad field: id
	}
}

Remember

  • Return errors as the last result and check them right away
  • Wrap with fmt.Errorf("context: %w", err)
  • Use errors.Is for sentinels and errors.As for error types

Watch out for

  • Comparing err.Error() strings or using == on wrapped errors instead of errors.Is.

Check yourself

  1. `err := fmt.Errorf("read: %v", io.EOF)` — what does `errors.Is(err, io.EOF)` return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: go.dev/blog/go1.13-errors