eb9171368b
Added 'When to Use' subsections with concrete decision triggers and before/after Go code examples to patterns across all directories: - patterns/error-handling.md (3 patterns: sentinels, wrapping, Join) - patterns/concurrency.md (4 patterns: Mutex, Once, done channels, pipelines) - patterns/interfaces.md (4 patterns: small interfaces, accept/return, adapter, optional) - patterns/structs.md (3 patterns: zero-value, constructors, config structs) - patterns/package-design.md (3 patterns: internal/, init(), context keys) - patterns/style.md (3 patterns: interface checks, iota constants, named types) - patterns/testing-advanced.md (3 patterns: table tests, golden files, httptest) - patterns/api-conventions.md (3 patterns: Must, layered API, graceful shutdown) - patterns/documentation.md (2 patterns: examples, deprecated) - kubernetes/patterns.md (3 patterns: controller, workqueue, leader election) - kubernetes/production-go.md (2 patterns: codegen, HandleCrash) - smells/anti-patterns.md (2 anti-patterns: cache mutation, edge-triggered)
626 lines
16 KiB
Markdown
626 lines
16 KiB
Markdown
# Go Error Handling Patterns
|
|
|
|
Patterns extracted from the Go standard library source code.
|
|
|
|
---
|
|
|
|
## 1. Sentinel Errors
|
|
|
|
### Source: `src/io/io.go:40-43` (EOF), `src/errors/errors.go:81-83` (ErrUnsupported)
|
|
|
|
```go
|
|
// src/io/io.go:40-43
|
|
// EOF is the error returned by Read when no more input is available.
|
|
// (Read must return EOF itself, not an error wrapping EOF,
|
|
// because callers will test for EOF using ==.)
|
|
var EOF = errors.New("EOF")
|
|
|
|
// src/io/io.go:47-49
|
|
var ErrUnexpectedEOF = errors.New("unexpected EOF")
|
|
```
|
|
|
|
```go
|
|
// src/errors/errors.go:81-83
|
|
var ErrUnsupported = New("unsupported operation")
|
|
```
|
|
|
|
### Why
|
|
|
|
Sentinel errors are package-level values that represent specific, well-known error conditions. They enable callers to test for specific failures:
|
|
|
|
```go
|
|
if err == io.EOF {
|
|
// end of input — not an error, just done
|
|
}
|
|
```
|
|
|
|
**Critical rule from io.EOF's doc comment**: Read must return EOF itself, **not an error wrapping EOF**, because callers test for it with `==`. This is the distinction between sentinel errors (identity-checked) and wrapped errors (tree-checked).
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- You have a specific, well-known failure condition callers need to check by identity
|
|
- Multiple packages compare against the same error value (`io.EOF`, `sql.ErrNoRows`)
|
|
- The error represents a **state** ("end of stream", "not found"), not a bug
|
|
|
|
**Example — before:**
|
|
```go
|
|
func fetchUser(id int) (*User, error) {
|
|
row := db.QueryRow("SELECT ...")
|
|
var u User
|
|
err := row.Scan(&u.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user not found") // caller can't distinguish "not found" from "db down"
|
|
}
|
|
return &u, nil
|
|
}
|
|
```
|
|
|
|
**Example — after:**
|
|
```go
|
|
var ErrUserNotFound = errors.New("users: not found")
|
|
|
|
func fetchUser(id int) (*User, error) {
|
|
row := db.QueryRow("SELECT ...")
|
|
var u User
|
|
err := row.Scan(&u.Name)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrUserNotFound // sentinel: callers can test with errors.Is
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetchUser: %w", err)
|
|
}
|
|
return &u, nil
|
|
}
|
|
```
|
|
|
|
### Anti-pattern
|
|
|
|
```go
|
|
// DON'T: Use string matching
|
|
if err.Error() == "EOF" { ... } // fragile, not guaranteed
|
|
|
|
// DON'T: Return a new error each time for sentinel conditions
|
|
func Read() error {
|
|
return errors.New("EOF") // every call creates a new value, can't compare with ==
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 2. errors.New — Minimal Error Construction
|
|
|
|
### Source: `src/errors/errors.go:62-69`
|
|
|
|
```go
|
|
// src/errors/errors.go:62-64
|
|
func New(text string) error {
|
|
return &errorString{text}
|
|
}
|
|
|
|
// src/errors/errors.go:66-69
|
|
type errorString struct {
|
|
s string
|
|
}
|
|
|
|
func (e *errorString) Error() string {
|
|
return e.s
|
|
}
|
|
```
|
|
|
|
### Why
|
|
|
|
`errors.New` returns a pointer to a private struct. Each call creates a **distinct value** even with identical text — this is intentional for sentinel errors. Two calls to `errors.New("foo")` produce different errors (`!=`).
|
|
|
|
The `error` interface itself is the smallest possible:
|
|
```go
|
|
type error interface {
|
|
Error() string
|
|
}
|
|
```
|
|
|
|
### Anti-pattern
|
|
|
|
```go
|
|
// DON'T: Export the error type
|
|
type MyError string // callers can create values that accidentally == your sentinels
|
|
|
|
// DON'T: Use plain strings as errors
|
|
func doThing() error {
|
|
return "something failed" // doesn't implement error interface
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Error Wrapping with fmt.Errorf and %w
|
|
|
|
### Source: `src/fmt/errors.go:13-23`, `src/fmt/errors.go:70-80`
|
|
|
|
```go
|
|
// src/fmt/errors.go:13-23
|
|
// Errorf formats according to a format specifier and returns the string
|
|
// as a value that satisfies error.
|
|
//
|
|
// If the format specifier includes a %w verb with an error operand,
|
|
// the returned error will implement an Unwrap method returning the operand.
|
|
// If there is more than one %w verb, the returned error will implement an
|
|
// Unwrap method returning a []error containing all the %w operands.
|
|
func Errorf(format string, a ...any) (err error) { ... }
|
|
|
|
// src/fmt/errors.go:70-80
|
|
type wrapError struct {
|
|
msg string
|
|
err error
|
|
}
|
|
|
|
func (e *wrapError) Error() string {
|
|
return e.msg
|
|
}
|
|
|
|
func (e *wrapError) Unwrap() error {
|
|
return e.err
|
|
}
|
|
```
|
|
|
|
### Why
|
|
|
|
`%w` creates an error chain: the returned error wraps the original. `errors.Is` and `errors.As` walk this chain. Use `%w` when callers should be able to inspect the underlying cause.
|
|
|
|
```go
|
|
// Wraps: callers can detect the original error
|
|
return fmt.Errorf("open config: %w", err)
|
|
|
|
// Does NOT wrap: hides the original error
|
|
return fmt.Errorf("open config: %v", err)
|
|
```
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- You're adding context to an error before returning it up the call stack
|
|
- The caller's error message would be meaningless without knowing *what* operation failed
|
|
- You have a chain of function calls and want a readable error trail: `"open config: read file: permission denied"`
|
|
|
|
**Example — before:**
|
|
```go
|
|
func loadConfig(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err // caller sees "open /etc/app.conf: permission denied" — no context about WHO called ReadFile
|
|
}
|
|
var cfg Config
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
return nil, err // caller can't tell if this was a read error or a parse error
|
|
}
|
|
return &cfg, nil
|
|
}
|
|
```
|
|
|
|
**Example — after:**
|
|
```go
|
|
func loadConfig(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load config: %w", err) // wraps: callers can errors.Is(err, os.ErrNotExist)
|
|
}
|
|
var cfg Config
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
return nil, fmt.Errorf("load config: parse %s: %v", path, err) // %v: hides internal JSON error type
|
|
}
|
|
return &cfg, nil
|
|
}
|
|
```
|
|
|
|
### When to use %w vs %v
|
|
|
|
- **%w**: When the wrapped error is part of your API contract. Callers can depend on it.
|
|
- **%v**: When you want to include the error text but NOT let callers depend on the underlying type. Use for implementation details.
|
|
|
|
### Anti-pattern
|
|
|
|
```go
|
|
// DON'T: Lose the original error
|
|
return errors.New("failed to open config") // original error vanished
|
|
|
|
// DON'T: Wrap errors that aren't part of your contract with %w
|
|
return fmt.Errorf("internal: %w", internalErr) // now callers depend on internalErr's type
|
|
```
|
|
|
|
---
|
|
|
|
## 4. errors.Is — Checking Error Identity Through Chains
|
|
|
|
### Source: `src/errors/wrap.go:30-44`
|
|
|
|
```go
|
|
// src/errors/wrap.go:30-44
|
|
func Is(err, target error) bool {
|
|
if err == nil || target == nil {
|
|
return err == target
|
|
}
|
|
isComparable := reflectlite.TypeOf(target).Comparable()
|
|
return is(err, target, isComparable)
|
|
}
|
|
|
|
func is(err, target error, targetComparable bool) bool {
|
|
for {
|
|
if targetComparable && err == target {
|
|
return true
|
|
}
|
|
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
|
|
return true
|
|
}
|
|
switch x := err.(type) {
|
|
case interface{ Unwrap() error }:
|
|
err = x.Unwrap()
|
|
if err == nil {
|
|
return false
|
|
}
|
|
case interface{ Unwrap() []error }:
|
|
for _, err := range x.Unwrap() {
|
|
if is(err, target, targetComparable) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Why
|
|
|
|
`errors.Is` walks the entire error tree (depth-first). It checks:
|
|
1. Direct equality (`err == target`)
|
|
2. Custom `Is(error) bool` method on the error
|
|
3. Then unwraps and recurses
|
|
|
|
This means wrapped errors are transparent:
|
|
```go
|
|
err := fmt.Errorf("config: %w", os.ErrNotExist)
|
|
errors.Is(err, os.ErrNotExist) // true! walks the chain
|
|
```
|
|
|
|
### Anti-pattern
|
|
|
|
```go
|
|
// DON'T: Use == directly on potentially-wrapped errors
|
|
if err == os.ErrNotExist { ... } // fails if err wraps ErrNotExist
|
|
|
|
// DO: Use errors.Is
|
|
if errors.Is(err, os.ErrNotExist) { ... } // works through wrapping
|
|
```
|
|
|
|
---
|
|
|
|
## 5. errors.As — Extracting Error Types Through Chains
|
|
|
|
### Source: `src/errors/wrap.go:96-120`
|
|
|
|
```go
|
|
// src/errors/wrap.go:96-120
|
|
func As(err error, target any) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
// ... validation ...
|
|
targetType := typ.Elem()
|
|
return as(err, target, val, targetType)
|
|
}
|
|
```
|
|
|
|
The `as` function (line 121+) walks the tree checking `AssignableTo` and custom `As(any) bool` methods.
|
|
|
|
### Why
|
|
|
|
Extract specific error types from wrapped chains:
|
|
|
|
```go
|
|
var pathErr *fs.PathError
|
|
if errors.As(err, &pathErr) {
|
|
fmt.Println("failed path:", pathErr.Path)
|
|
}
|
|
```
|
|
|
|
### Go 1.24+: errors.AsType (generic version)
|
|
|
|
From `src/errors/errors.go:48-56` doc:
|
|
```go
|
|
if perr, ok := errors.AsType[*fs.PathError](err); ok {
|
|
fmt.Println(perr.Path)
|
|
}
|
|
```
|
|
|
|
### Anti-pattern
|
|
|
|
```go
|
|
// DON'T: Type-assert directly on potentially-wrapped errors
|
|
if pathErr, ok := err.(*fs.PathError); ok { ... } // fails if wrapped
|
|
|
|
// DO: Use errors.As
|
|
var pathErr *fs.PathError
|
|
if errors.As(err, &pathErr) { ... } // works through wrapping
|
|
```
|
|
|
|
---
|
|
|
|
## 6. errors.Join — Multi-Error Aggregation
|
|
|
|
### Source: `src/errors/join.go:20-39`
|
|
|
|
```go
|
|
// src/errors/join.go:20-39
|
|
func Join(errs ...error) error {
|
|
n := 0
|
|
for _, err := range errs {
|
|
if err != nil {
|
|
n++
|
|
}
|
|
}
|
|
if n == 0 {
|
|
return nil
|
|
}
|
|
e := &joinError{
|
|
errs: make([]error, 0, n),
|
|
}
|
|
for _, err := range errs {
|
|
if err != nil {
|
|
e.errs = append(e.errs, err)
|
|
}
|
|
}
|
|
return e
|
|
}
|
|
```
|
|
|
|
The `joinError` type implements `Unwrap() []error`, making both `Is` and `As` traverse correctly.
|
|
|
|
### Why
|
|
|
|
For operations that can produce multiple errors (closing multiple resources, validating multiple fields), `Join` collects them into a single error.
|
|
|
|
```go
|
|
var errs []error
|
|
errs = append(errs, closeDB())
|
|
errs = append(errs, closeCache())
|
|
return errors.Join(errs...) // nil if all nil
|
|
```
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- You're closing/cleaning up multiple resources and each can fail independently
|
|
- A validation function checks multiple fields and you want ALL errors, not just the first
|
|
- You're running parallel operations and collecting errors from each
|
|
|
|
**Example — before:**
|
|
```go
|
|
func cleanup(db *sql.DB, cache *redis.Client, file *os.File) error {
|
|
if err := db.Close(); err != nil {
|
|
return err // stops here — cache and file leak!
|
|
}
|
|
if err := cache.Close(); err != nil {
|
|
return err // file still leaks
|
|
}
|
|
return file.Close()
|
|
}
|
|
```
|
|
|
|
**Example — after:**
|
|
```go
|
|
func cleanup(db *sql.DB, cache *redis.Client, file *os.File) error {
|
|
return errors.Join(
|
|
db.Close(),
|
|
cache.Close(),
|
|
file.Close(),
|
|
) // nil if all nil; contains all failures otherwise
|
|
}
|
|
```
|
|
|
|
### Anti-pattern
|
|
|
|
```go
|
|
// DON'T: Return only the last error
|
|
var lastErr error
|
|
for _, r := range resources {
|
|
if err := r.Close(); err != nil {
|
|
lastErr = err // silently loses previous errors
|
|
}
|
|
}
|
|
return lastErr
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Custom Is() Method — Equivalence Classes
|
|
|
|
### Source: `src/errors/wrap.go:42-44` (doc comment), `src/context/context.go:177-179`
|
|
|
|
From the `errors.Is` doc:
|
|
```go
|
|
// An error type might provide an Is method so it can be treated as
|
|
// equivalent to an existing error. For example, if MyError defines
|
|
//
|
|
// func (m MyError) Is(target error) bool { return target == fs.ErrExist }
|
|
//
|
|
// then Is(MyError{}, fs.ErrExist) returns true.
|
|
```
|
|
|
|
Real example from context:
|
|
```go
|
|
// src/context/context.go:177-179
|
|
type deadlineExceededError struct{}
|
|
|
|
func (deadlineExceededError) Error() string { return "context deadline exceeded" }
|
|
func (deadlineExceededError) Timeout() bool { return true }
|
|
func (deadlineExceededError) Temporary() bool { return true }
|
|
```
|
|
|
|
### Why
|
|
|
|
Custom `Is` methods let you define error equivalence beyond pointer identity. A `syscall.Errno` can match `fs.ErrExist` through its `Is` method, bridging OS-specific error codes to portable sentinel errors.
|
|
|
|
### Anti-pattern
|
|
|
|
```go
|
|
// DON'T: Make Is() too broad
|
|
func (e MyError) Is(target error) bool {
|
|
return true // matches everything — defeats the purpose
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Error Wrapping in Custom Types (Unwrap pattern)
|
|
|
|
### Source: `src/encoding/json/encode.go:276-293`
|
|
|
|
```go
|
|
// src/encoding/json/encode.go:276-282
|
|
type MarshalerError struct {
|
|
Type reflect.Type
|
|
Err error
|
|
sourceFunc string
|
|
}
|
|
|
|
// src/encoding/json/encode.go:293
|
|
func (e *MarshalerError) Unwrap() error { return e.Err }
|
|
```
|
|
|
|
### Why
|
|
|
|
Custom error types carry structured data (which type failed, which function) while still participating in the error chain via `Unwrap()`. Callers can use `errors.As` to extract the `MarshalerError` AND use `errors.Is` to check the underlying cause.
|
|
|
|
### Pattern Template
|
|
|
|
```go
|
|
type OpError struct {
|
|
Op string
|
|
Path string
|
|
Err error
|
|
}
|
|
|
|
func (e *OpError) Error() string {
|
|
return e.Op + " " + e.Path + ": " + e.Err.Error()
|
|
}
|
|
|
|
func (e *OpError) Unwrap() error {
|
|
return e.Err
|
|
}
|
|
```
|
|
|
|
### Anti-pattern
|
|
|
|
```go
|
|
// DON'T: Store error as string
|
|
type MyError struct {
|
|
Message string // lost the original error!
|
|
}
|
|
|
|
// DON'T: Forget to implement Unwrap
|
|
type MyError struct {
|
|
Err error // has the error but errors.Is can't traverse it
|
|
}
|
|
func (e *MyError) Error() string { return e.Err.Error() }
|
|
// Missing: func (e *MyError) Unwrap() error { return e.Err }
|
|
```
|
|
|
|
---
|
|
|
|
## 9. ErrUnsupported — Feature Detection via Errors
|
|
|
|
### Source: `src/errors/errors.go:76-83`
|
|
|
|
```go
|
|
// src/errors/errors.go:76-83
|
|
// ErrUnsupported indicates that a requested operation cannot be performed,
|
|
// because it is unsupported.
|
|
//
|
|
// Functions and methods should not return this error but should instead
|
|
// return an error including appropriate context that satisfies
|
|
//
|
|
// errors.Is(err, errors.ErrUnsupported)
|
|
//
|
|
// either by directly wrapping ErrUnsupported or by implementing an Is method.
|
|
var ErrUnsupported = New("unsupported operation")
|
|
```
|
|
|
|
### Why
|
|
|
|
This pattern separates "what happened" (detailed context) from "what kind of failure" (sentinel identity). Return a rich error that *wraps* or *matches* the sentinel:
|
|
|
|
```go
|
|
return fmt.Errorf("chmod %s: %w", path, errors.ErrUnsupported)
|
|
```
|
|
|
|
### Anti-pattern
|
|
|
|
```go
|
|
// DON'T: Return the sentinel directly without context
|
|
return errors.ErrUnsupported // no info about what operation or why
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Error String Conventions
|
|
|
|
### Source: `src/net/http/server.go:39-56`
|
|
|
|
```go
|
|
// src/net/http/server.go:39-56
|
|
var (
|
|
ErrHijacked = errors.New("http: connection has been hijacked")
|
|
ErrContentLength = errors.New("http: wrote more than the declared Content-Length")
|
|
)
|
|
```
|
|
|
|
### Convention: Error String Format
|
|
|
|
```
|
|
package: description
|
|
```
|
|
|
|
- Lowercase (no capital first letter)
|
|
- No trailing punctuation
|
|
- Package prefix for disambiguation
|
|
|
|
### Anti-pattern
|
|
|
|
```go
|
|
// DON'T: Capitalize error strings
|
|
errors.New("Connection has been hijacked")
|
|
|
|
// DON'T: End with punctuation
|
|
errors.New("connection failed.")
|
|
|
|
// DON'T: Include redundant "error" word
|
|
errors.New("http error: connection failed") // it's already an error
|
|
```
|
|
|
|
---
|
|
|
|
## Summary: Error Handling Decision Tree
|
|
|
|
```
|
|
Is this a specific, well-known condition?
|
|
├── YES → Sentinel error (package-level var)
|
|
│ └── Should callers detect it? → errors.Is
|
|
└── NO → Is there structured info to convey?
|
|
├── YES → Custom error type with Unwrap()
|
|
│ └── Should callers extract it? → errors.As
|
|
└── NO → fmt.Errorf with %w (wraps) or %v (doesn't wrap)
|
|
```
|
|
|
|
| When to... | Use |
|
|
|---|---|
|
|
| Create a well-known error condition | `var ErrFoo = errors.New("pkg: foo")` |
|
|
| Add context while preserving cause | `fmt.Errorf("doing X: %w", err)` |
|
|
| Add context, hide internal cause | `fmt.Errorf("doing X: %v", err)` |
|
|
| Check for a specific condition | `errors.Is(err, ErrFoo)` |
|
|
| Extract structured error data | `errors.As(err, &target)` |
|
|
| Aggregate multiple errors | `errors.Join(err1, err2)` |
|
|
| Make custom types traversable | Implement `Unwrap() error` |
|
|
| Define error equivalence | Implement `Is(error) bool` |
|