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?