| name | go-error-handling |
| description | Go error handling patterns, wrapping, sentinel errors, custom error types, and the errors package. Grounded in Effective Go, Go Code Review Comments, and production-proven idioms. Use when implementing error handling, designing error types, debugging error chains, or reviewing error handling patterns. Trigger examples: "handle errors", "error wrapping", "custom error type", "sentinel errors", "errors.Is", "errors.As". Do NOT use for panic/recover patterns in middleware (use go-api-design) or test assertion errors (use go-test-quality).
|
| license | MIT |
| metadata | {"version":"1.1.0"} |
Go Error Handling
Go's explicit error handling is a feature, not a limitation.
These patterns ensure errors are informative, actionable, and properly propagated.
1. Error Decision Tree
When creating or returning an error, follow this tree:
- Simple, no extra context needed? →
errors.New("message")
- Need to add context to existing error? →
fmt.Errorf("doing X: %w", err)
- Caller needs to detect this error? → Sentinel
var or custom type
- Error carries structured data? → Custom type implementing
error
- Propagating from downstream? → Wrap with
%w and add context
2. Sentinel Errors
Use package-level var for errors that callers need to check:
var (
ErrNotFound = errors.New("user: not found")
ErrUnauthorized = errors.New("user: unauthorized")
)
Callers check with errors.Is:
if errors.Is(err, user.ErrNotFound) {
}
NEVER compare errors with ==. Always use errors.Is().
3. Custom Error Types
When errors need to carry structured information:
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: field %s: %s", e.Field, e.Message)
}
var valErr *ValidationError
if errors.As(err, &valErr) {
log.Printf("invalid field: %s", valErr.Field)
}
4. Error Wrapping
ALWAYS add context when propagating errors up the stack.
Use %w to preserve the error chain:
func getUser(id int64) (*User, error) {
row, err := db.QueryRow(ctx, query, id)
if err != nil {
return nil, fmt.Errorf("get user %d: %w", id, err)
}
}
return nil, err
return nil, fmt.Errorf("failed: %v", err)
When NOT to use %w
Use %v instead of %w when you explicitly want to break the error chain,
preventing callers from depending on internal implementation errors:
return nil, fmt.Errorf("user lookup failed: %v", err)
5. Handle Errors Exactly Once
An error should be either logged OR returned, never both:
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load config %s: %w", path, err)
}
}
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
log.Printf("failed to read config: %v", err)
return nil, err
}
}
The rule: the component that decides what to do about the error is the one
that logs/metrics it. Everyone else wraps and returns.
6. Error Naming Conventions
var ErrNotFound = errors.New("not found")
type NotFoundError struct { ... }
type ValidationError struct { ... }
errors.New("auth: token expired")
fmt.Errorf("user: get by id %d: %w", id, err)
7. Panic Rules
Panic is NOT error handling. Use panic only when:
- Program initialization fails and cannot continue (
template.Must, flag parsing)
- Programmer error that should never happen (violated invariant)
- Nil dereference that indicates a bug, not a runtime condition
In tests, use t.Fatal / t.FailNow, never panic.
In HTTP handlers and middleware, recover from panics at the boundary
to prevent one request from crashing the server.
8. Error Checking Patterns
if err := doSomething(); err != nil {
return fmt.Errorf("do something: %w", err)
}
func process() (result string, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("process: %w", err)
}
}()
}
var errs []error
for _, item := range items {
if err := validate(item); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
Executable Verification
After writing or reviewing error handling, run the linters that prove
the rules above (skip any that is not installed and note it):
go vet ./...
golangci-lint run --enable errcheck,errorlint
Verification Checklist
- No
_ discarding errors (unless explicitly justified with comment)
- Every
fmt.Errorf wrapping uses %w (or %v with documented reason)
- Sentinel errors use
var Err... naming
- Custom error types implement
error interface
- Callers use
errors.Is / errors.As, never == or type assertion
- No log-and-return patterns
- Error messages are lowercase, contextual, chain-friendly