Concept · Go

Errors as values and wrapping

Return errors, add context with %w, and inspect the chain with errors.Is and errors.As.

3 minerrors.Iserrors.As%wsentinel errors

Go has no exceptions for ordinary failures. A function that can fail returns an error as its last value, and the caller checks it right away:

go
cfg, err := loadConfig(path)
if err != nil {
	return fmt.Errorf("load config %q: %w", path, err)
}

error is just an interface with one method, Error() string. That makes errors ordinary values you can return, store, compare and wrap.

Wrapping adds context

Each layer adds what it was doing when the error happened, and %w keeps the original error inside:

text
create order: reserve stock for sku "A-17": query items: context deadline exceeded

That one line reads like a stack trace written for humans. Make each layer's message short: say what was being attempted, without starting with "failed to" or "error".

Inspecting the chain

Never compare error strings. Use the two functions that walk the chain of wrapped errors:

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

if errors.Is(err, ErrNotFound) { /* 404 */ }     // is any error in the chain this value?
if errors.Is(err, context.DeadlineExceeded) { /* timed out */ }

// custom type with fields: is any error in the chain a *pgconn.PgError? (Go 1.26+)
if pgErr, ok := errors.AsType[*pgconn.PgError](err); ok && pgErr.Code == "23505" { /* unique violation → 409 */ }
  • Sentinel values (io.EOF, sql.ErrNoRows) are for conditions callers branch on.
  • Error types carry data (a code, a field name), and you check them with errors.AsType. Before Go 1.26, declare a variable and pass its address to errors.As(err, &pgErr); you'll still see that form everywhere.
  • errors.Join(a, b) combines several errors, and Is/As look inside every one of them.

Habits

  • Handle an error once. Either log it or return it, not both, or you'll log the same failure at every layer.
  • Use %w deliberately. Wrapping makes the inner error part of your API, because callers can now errors.Is against it. Use %v when you don't want callers depending on an implementation detail, such as the database driver's error type.
  • Translate at the boundary. Map domain errors to status codes in one place, usually middleware or a helper that writes responses, rather than in every handler.
  • panic is for programmer bugs and impossible states, not for "file not found."

Context cancellation errors are ordinary values too: check errors.Is(err, context.Canceled) to tell "the client went away" from a real failure.

Learn it in depth