๐ŸŽError Wrapping & Sentinel ErrorsLESSON

Error Wrapping & Sentinel Errors

Why wrap errors?

Returning a raw error loses context โ€” the caller sees "connection refused" with no indication of which operation failed. Wrapping adds context while preserving the original error for programmatic inspection.

fmt.Errorf with %w

The %w verb wraps the error. The caller sees "readConfig /etc/app.yaml: open /etc/app.yaml: no such file or directory" โ€” a readable chain โ€” and can still unwrap the original *os.PathError.

Sentinel errors

Sentinel errors are package-level variables that represent well-known error conditions:

Callers compare with errors.Is:

errors.Is โ€” check along the chain

errors.Is unwraps the error chain recursively until it finds a match:

Never compare errors with == โ€” it fails for wrapped errors. Always use errors.Is.

errors.As โ€” extract a specific type

errors.As unwraps the chain and type-asserts each error until it finds one matching the target type:

The target must be a pointer to the error type you want. errors.As sets the target if found.

Custom error types with Unwrap

A custom error type participates in the chain by implementing Unwrap() error:

Now errors.Is(queryErr, sql.ErrNoRows) works even when wrapped in QueryError.

errors.Join (Go 1.20+)

Combine multiple errors into one:

The wrapping convention

Follow this pattern for error messages in functions:

  • "operationName arg: %w" โ€” colon-space before the wrapped error
  • No capital letter at the start (errors are concatenated mid-sentence)
  • No trailing period

Knowledge Check

What is the difference between errors.Is and == for comparing errors?

What does the %w verb in fmt.Errorf do that %v does not?

Why must a custom error type implement Unwrap() error to work with errors.Is and errors.As?