go-error-handling · eduardo-sl — Claude Code skill for Go error handling
A skill that tells your agent how to create, wrap and inspect Go errors: %w wrapping, sentinel errors, custom types, errors.Is and errors.As.
npx skills add eduardo-sl/go-agent-skills --skill go-error-handling -a claude-codeWhat go-error-handling does
The skill covers the whole path of an error through a Go program. A decision tree picks the right tool: errors.New for a plain failure, fmt.Errorf with %w when context has to be added, a package-level sentinel when callers must recognise the failure, a custom type when the error carries structured data.
There are sections on Unwrap for wrapper types, on handling an error exactly once (log it or return it, not both), on naming — Err for sentinels, an Error suffix for types, lowercase messages without punctuation — and on module boundaries, where %w publishes the wrapped error as part of your API and %v keeps it private. Panic gets its own rules: broken initialisation and violated invariants only.
The skill assumes a Go project, so the Go toolchain has to be present — the go and gofmt commands in its allowed tools do nothing without it. Longer material sits in references/patterns-and-boundaries.md and loads on demand. A project rule that explicitly supersedes this skill wins over it.
What it helps with
- wrap a database error so the caller can still detect it
- add a sentinel error for a missing record and check it with
errors.Is - define a custom error type with fields without breaking the
Unwrapchain - stop logging an error and returning it at the same time
- decide whether an internal error should reach the caller or stay hidden behind
%v
Example prompts for the agent
- Wrap the errors in
getUserwith%wand add the id to the context - Add sentinel errors for not found and unauthorized, then check them with
errors.Is - Create a
ValidationErrortype with aFieldfield and extract it witherrors.As
How to install go-error-handling
1Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-error-handling -a claude-code2Manually: put the files in place
~/.claude/skills/go-error-handling/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/go-error-handling/
1Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-error-handling -a codex2Manually: put the files in place
.agents/skills/go-error-handling/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-error-handling -a cursor2Manually: put the files in place
.cursor/skills/go-error-handling/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install eduardo-sl/go-agent-skills go-error-handling2Manually: put the files in place
.github/skills/go-error-handling/SKILL.md1Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-error-handling -a gemini-cli2Platform CLI
gemini skills install https://github.com/eduardo-sl/go-agent-skills --path 'skills/(code-quality)/go-error-handling'Manually: put the files in place
.devin/skills/go-error-handling/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-error-handling -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about go-error-handling
How do I install go-error-handling in Claude Code?
Run npx skills add eduardo-sl/go-agent-skills --skill go-error-handling -a claude-code. The skill lands in ~/.claude/skills/go-error-handling/SKILL.md.
What does go-error-handling do?
It gives the agent a set of Go error rules: a decision tree for creating errors, %w wrapping, sentinel errors, custom types with Unwrap, naming, and how errors behave at module boundaries.
How is it different from go-api-design and go-test-quality?
This one is about error handling in ordinary code. Panic and recover in middleware belong to go-api-design, and test assertions belong to go-test-quality.
Source text
SKILL.md · MIT
---
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
© eduardo-sl · MIT · eduardo-sl/go-agent-skills
Similar skills
All skills for Programming languages →go-error-handling by other authors comes first.