Go

Design custom errors in Go. Explain wrapping, errors.Is, errors.As and multi-error trees.

Question 98HardGo 1.22 to 1.25

error is a one-method interface. Go offers three styles, each for a different need:

  • Sentinel errors (var ErrNotFound = errors.New(...)), compared by identity.
  • Typed errors (structs), which carry extra data.
  • Opaque errors, which callers only test for behaviour.

Wrapping with %w keeps a chain that the helper functions can walk.

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

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 }

func find() error {
    return fmt.Errorf("repo: %w", &QueryError{"SELECT 1", ErrNotFound})
}

err := find()
fmt.Println(errors.Is(err, ErrNotFound)) // true: walks Unwrap chain

var qe *QueryError
if errors.As(err, &qe) {                 // target is **QueryError
    fmt.Println(qe.Query)                // SELECT 1
}

// Go 1.20: multiple wrapping forms a tree
joined := errors.Join(ErrNotFound, context.Canceled)
both := fmt.Errorf("a: %w, b: %w", ErrNotFound, io.EOF)
fmt.Println(errors.Is(joined, context.Canceled), errors.Is(both, io.EOF)) // true true

Gotchas:

  • The target of errors.As must be a non-nil pointer to a type that implements error, or to an interface type. Anything else panics.
  • %v formats only the error text, so callers cannot match the cause. %w exposes the wrapped error to errors.Is/As, which makes it part of your API contract. Choose deliberately.
  • Custom Is(target error) bool and As(any) bool methods let you customise matching.
  • Comparing err == ErrX directly fails once the error has been wrapped.
  • Unwrap() []error is the multi-error form. errors.Is and errors.As walk the tree depth-first.

More on Interfaces, Methods & Embedding

All 35 Interfaces, Methods & Embedding questions