← GophersCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Gophers
Snapshot Sep 30, 2026 · 23:14 UTC · version 0.1.0
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Use when writing, wrapping, inspecting, or logging Go errors. Covers strategy choice (sentinel vs typed vs opaque), wrapping with %w/%v, errors.Is/As/Join, the log-or-return rule, error strings, and panic/recover boundaries. Apply proactively whenever a function returns or accepts an error, even if the user has not asked about error handling.",
"included_files": [
{
"relative_path": "agents/openai.yaml",
"size_in_bytes": 225
},
{
"relative_path": "references/strategy-decision.md",
"size_in_bytes": 2674
},
{
"relative_path": "references/typed-nil-trap.md",
"size_in_bytes": 2064
},
{
"relative_path": "references/wrapping-vs-shadowing.md",
"size_in_bytes": 2912
}
],
"name": "go-error-handling",
"skill_md_contents": "---\nname: go-error-handling\ndescription: \"Use when writing, wrapping, inspecting, or logging Go errors. Covers strategy choice (sentinel vs typed vs opaque), wrapping with %w/%v, errors.Is/As/Join, the log-or-return rule, error strings, and panic/recover boundaries. Apply proactively whenever a function returns or accepts an error, even if the user has not asked about error handling.\"\nlicense: MIT\ncompatibility: \"Designed for Claude Code or similar AI coding agents. Requires Go 1.20+ for errors.Join. Wrapping (%w, errors.Is/As) requires Go 1.13+.\"\nallowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)\n---\n\n# Go Error Handling\n\nErrors in Go are values. Treat them as part of the API: choose a strategy per failure mode, propagate with wrapping, inspect with `errors.Is`/`As`, and handle each error **exactly once**.\n\n## Core Rules\n\n1. **Errors are values, not exceptions.** Return them; do not panic across API boundaries.\n2. **Handle each error exactly once.** *Either* log it *or* return it — never both.\n3. **The caller decides what is exceptional.** Library code returns; binaries (or top-level handlers) decide whether to log, retry, or exit.\n4. **Wrap only when you add real context.** A wrap that just repeats the function name is noise. Use `%w` to preserve identity; `%v` to deliberately hide an unstable type.\n\n## Strategy Decision\n\nPick the simplest strategy that meets the caller's needs:\n\n| Strategy | When to use | Example |\n|---|---|---|\n| **Opaque error** (default) | Caller only needs to know *something* failed | `errors.New(\"invalid input\")` |\n| **Sentinel error** | Caller needs to test for a specific named condition | `io.EOF`, `sql.ErrNoRows` |\n| **Typed error** | Caller needs structured fields (path, code, retry-after) | `*os.PathError`, `*url.Error` |\n| **Joined errors** | A single operation produced several independent failures | `errors.Join(errA, errB)` |\n\n> Read [references/strategy-decision.md](references/strategy-decision.md) when the caller's needs are unclear or when migrating between strategies without breaking callers.\n\n## Writing Errors\n\n### Strings\n\n- **Lowercase, no trailing punctuation.** Errors are composed: `fmt.Errorf(\"write %s: %w\", path, err)` reads as one sentence.\n- **Be specific.** `\"open config: permission denied\"` beats `\"failed to open file\"`.\n- **Do not include the function name.** Stack context is added by wrapping at each layer.\n\n### Creating\n\n```go\n// Opaque — the caller only checks != nil\nreturn errors.New(\"invalid character in token\")\n\n// Sentinel — exported, package-level, named ErrXxx\nvar ErrNotFound = errors.New(\"user: not found\")\n\n// Typed — when callers need structured fields\ntype ValidationError struct {\n Field string\n Rule string\n}\nfunc (e *ValidationError) Error() string {\n return fmt.Sprintf(\"validation: %s violates %s\", e.Field, e.Rule)\n}\n```\n\n## Wrapping and Inspection\n\n### Wrap with `%w` to add context while preserving identity\n\n```go\nif err := db.Get(id); err != nil {\n return fmt.Errorf(\"loading user %d: %w\", id, err)\n}\n```\n\n### Inspect with `errors.Is` (identity) and `errors.As` (type)\n\n```go\nif errors.Is(err, sql.ErrNoRows) { /* handled */ }\n\nvar ve *ValidationError\nif errors.As(err, &ve) {\n return reply.BadRequest(ve.Field)\n}\n```\n\n**Never** compare error strings (`err.Error() == \"...\"`) — strings are not stable API.\n\n### Join independent failures\n\n```go\nerrs := errors.Join(\n validate(name),\n validate(email),\n validate(password),\n)\nif errs != nil {\n return errs // errors.Is/As walks both branches\n}\n```\n\n> Read [references/wrapping-vs-shadowing.md](references/wrapping-vs-shadowing.md) when deciding between `%w` (expose) and `%v` (hide), or when wrapping would leak an implementation detail.\n\n## Error Flow\n\n### Log or return — not both\n\n```go\n// Bad: caller will log it again, producing duplicate lines\nif err := svc.Do(ctx); err != nil {\n slog.ErrorContext(ctx, \"svc.Do failed\", \"err\", err)\n return err\n}\n\n// Good: log only at the boundary that decides the request is done\nif err := svc.Do(ctx); err != nil {\n return fmt.Errorf(\"doing svc work: %w\", err)\n}\n```\n\nThe HTTP handler / job runner / `main` is the only layer that logs.\n\n### Reduce nesting with guard clauses\n\n```go\n// Bad\nif err == nil {\n if x, ok := f(); ok {\n return x, nil\n }\n}\nreturn zero, err\n\n// Good\nif err != nil {\n return zero, err\n}\nx, ok := f()\nif !ok {\n return zero, errSomething\n}\nreturn x, nil\n```\n\n## Panic and Recover\n\n`panic` is for **programmer errors** (impossible states) and **package initialization**. It is never the right way to return a normal failure.\n\n```go\n// Acceptable: invariants the type guarantees\nfunc (q *Queue) MustEnqueue(v T) { if err := q.Enqueue(v); err != nil { panic(err) } }\n\n// Acceptable: recover at the goroutine boundary so one bad request cannot kill the server\ndefer func() {\n if r := recover(); r != nil {\n log.Error(\"panic recovered\", \"value\", r, \"stack\", debug.Stack())\n http.Error(w, \"internal error\", 500)\n }\n}()\n```\n\nDo **not** use `recover` to convert panics into errors as normal control flow.\n\nFor custom error types, implement `Unwrap() error` (or `Unwrap() []error` in Go 1.20+) so `errors.Is`/`As` can reach the cause. See [references/wrapping-vs-shadowing.md](references/wrapping-vs-shadowing.md#custom-unwrap).\n\n## Anti-Patterns\n\n| Anti-pattern | Why it hurts | Do this instead |\n|---|---|---|\n| `return errors.New(err.Error())` | Drops identity; `errors.Is` breaks | `return fmt.Errorf(\"ctx: %w\", err)` |\n| `if err.Error() == \"EOF\"` | String matching against unstable text | `errors.Is(err, io.EOF)` |\n| `_ = doThing()` | Silently swallows failures | Handle, log at boundary, or document why |\n| Returning `*MyError` (concrete pointer) | Typed-nil trap; non-nil interface | Return `error` (see [references/typed-nil-trap.md](references/typed-nil-trap.md)) |\n| Logging then returning the same error | Duplicate log lines, no single source of truth | Log only at the top boundary |\n| Wrapping at every layer with no new info | `\"a: b: c: d: real error\"` chains | Drop the wrap and `return err` |\n\n## Verification Checklist\n\nBefore finishing an error-handling change:\n\n- [ ] No `err.Error()` string comparisons\n- [ ] All wrapping uses `%w` (or `%v` is intentional and commented)\n- [ ] Functions return the `error` interface, not concrete types\n- [ ] Each error is logged at most once (at the request/job boundary)\n- [ ] Sentinels are package-level `var ErrXxx = errors.New(...)`\n- [ ] Typed errors expose only the fields callers actually need\n\n## References\n\n- [references/strategy-decision.md](references/strategy-decision.md) — picking opaque vs sentinel vs typed\n- [references/wrapping-vs-shadowing.md](references/wrapping-vs-shadowing.md) — `%w` vs `%v` decisions\n- [references/typed-nil-trap.md](references/typed-nil-trap.md) — why returning `*MyErr` breaks `== nil`\n"
}SHA-256 of public snapshot: 97e5c5243d42495391a18e27f11b2f32ab3bd85123d31c1a8d43db4cb86450e5