--- 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". Not for: panic/recover in middleware (go-api-design), test assertions (go-test-quality). user-invocable: true license: MIT compatibility: Designed for Claude Code or similar AI coding agents working on Go projects. Requires the Go toolchain. allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(gofmt:*) metadata: author: eduardo-sl version: "1.3.1" --- # Go Error Handling Go's explicit error handling is a feature, not a limitation. These patterns ensure errors are informative, actionable, and properly propagated. > Community default. A project skill or agent-config rule that explicitly > supersedes `go-error-handling` takes precedence over this one. Detailed reference material, loaded on demand: - `references/patterns-and-boundaries.md` — checking patterns, `Is` methods, translating errors at the HTTP boundary, low-cardinality logging, the Go 1.27 vet check on `%w` with pointer errors. Read a reference file only when the summary below is not enough. ## 1. Error Decision Tree When creating or returning an error, follow this tree: 1. **Simple, no extra context needed?** → `errors.New("message")` 2. **Need to add context to existing error?** → `fmt.Errorf("doing X: %w", err)` 3. **Caller needs to detect this error?** → Sentinel `var` or custom type 4. **Error carries structured data?** → Custom type implementing `error` 5. **Propagating from downstream?** → Wrap with `%w` and add context ## 2. Sentinel Errors Use package-level `var` for errors that callers need to check: ```go // ✅ Good — exported sentinel error var ( ErrNotFound = errors.New("user: not found") ErrUnauthorized = errors.New("user: unauthorized") ) // Naming convention: Err + Description // Prefix with package context in the message ``` Callers check with `errors.Is`: ```go if errors.Is(err, user.ErrNotFound) { // handle not found } ``` NEVER compare errors with `==`. Always use `errors.Is()`. ## 3. Custom Error Types When errors need to carry structured information: ```go type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation: field %s: %s", e.Field, e.Message) } // Callers extract it from the chain — Go 1.26+: if valErr, ok := errors.AsType[*ValidationError](err); ok { log.Printf("invalid field: %s", valErr.Field) } // Before Go 1.26: var valErr *ValidationError if errors.As(err, &valErr) { log.Printf("invalid field: %s", valErr.Field) } ``` A custom type that wraps a cause must expose it, or `errors.Is`/`errors.As` stop at your type and never see the error underneath: ```go type QueryError struct { Query string Err error } func (e *QueryError) Error() string { return "query " + e.Query + ": " + e.Err.Error() } func (e *QueryError) Unwrap() error { return e.Err } // keeps the chain walkable ``` ## 4. Error Wrapping ALWAYS add context when propagating errors up the stack. Use `%w` to preserve the error chain: ```go // ✅ Good — context added, chain preserved 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) } // ... } // ❌ Bad — no context return nil, err // ❌ Bad — chain broken, callers can't errors.Is/As 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: ```go // Intentionally hiding internal DB error from public API return nil, fmt.Errorf("user lookup failed: %v", err) ``` ## 5. Handle Errors Exactly Once An error should be either logged OR returned, never both: ```go // ✅ Good — return the error, let caller decide func loadConfig(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("load config %s: %w", path, err) } // ... } // ❌ Bad — log AND return (error handled twice) func loadConfig(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { log.Printf("failed to read config: %v", err) // handled once return nil, err // handled again } // ... } ``` 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 ```go // Sentinel errors: Err prefix var ErrNotFound = errors.New("not found") // Error types: Error suffix type NotFoundError struct { ... } type ValidationError struct { ... } // Error messages: lowercase, no punctuation, no "failed to" prefix // Include context: "package: action: detail" 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 - Scope the error to the `if` when nothing else needs it: `if err := do(); err != nil { return fmt.Errorf("do: %w", err) }`. - A deferred wrap on a named `err` result adds one prefix to every return path of a long function. - Collect independent failures (validation, parallel work) and return `errors.Join(errs...)` — `errors.Is`/`As` see every joined error. ## 9. Errors at Boundaries - **Publishing an error is a contract.** Inside the module, wrap with `%w`. At a boundary you publish (an exported library API, an RPC), `%w` makes the wrapped error part of your API; `%v` or a new error keeps it private. - **Translate before it reaches a user.** Map domain errors to a status and a stable message at the handler, and log the original once with its chain. `err.Error()` in a response leaks SQL, paths and hostnames. - **Keep logged messages low-cardinality.** The stable text is the message; IDs, paths and counts go in structured attributes so the log backend can group identical failures. - **Panics do not cross goroutines.** A `recover` only works in the goroutine that panicked — see go-concurrency-review. - **Implement `Is(target error) bool`** on a type to let `errors.Is` match a whole family of errors against one sentinel. Examples for sections 8 and 9: `references/patterns-and-boundaries.md`. ## 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): ```bash go vet ./... # includes printf %w misuse golangci-lint run --enable errcheck,errorlint # unchecked errors, %v-vs-%w, # == comparisons on errors ``` ## Verification Checklist 1. No `_` discarding errors (unless explicitly justified with comment) 2. Every `fmt.Errorf` wrapping uses `%w` (or `%v` with documented reason) 3. Sentinel errors use `var Err...` naming 4. Custom error types implement `error` interface 5. Callers use `errors.Is` / `errors.As` (`errors.AsType` on Go 1.26+), never `==` or type assertion 6. Custom types that wrap a cause implement `Unwrap() error` 7. No log-and-return patterns 8. Error messages are lowercase, contextual, chain-friendly 9. No `err.Error()` text in responses to users; logged messages are stable, details are attributes