Skip to main content
skill CX Codex

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.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add eduardo-sl/go-agent-skills --skill go-error-handling -a claude-code
Open on GitHub

What 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 Unwrap chain
  • 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 getUser with %w and add the id to the context
  • Add sentinel errors for not found and unauthorized, then check them with errors.Is
  • Create a ValidationError type with a Field field and extract it with errors.As

How to install go-error-handling

1Skills CLI (Vercel)

npx skills add eduardo-sl/go-agent-skills --skill go-error-handling -a claude-code

2Manually: put the files in place

~/.claude/skills/go-error-handling/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/go-error-handling/

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.

From the same set11

go-ci A skill that helps your AI agent write and set up GitHub Actions for Go projects. Use it when you need a pipeline with linting, tests, security checks, and caching. CX Codex +1 ★ 83repo stars git-commit A skill that makes your agent write commit messages in Conventional Commits style: typed, scoped, imperative. Useful when the repo history has turned into noise. CX Codex +1 ★ 83repo stars go-troubleshooting A skill that walks the agent through Go runtime diagnosis: panics, deadlocks, goroutine and memory leaks, race reports. Useful when a program crashes or hangs and the cause is unclear. CX Codex +1 ★ 83repo stars go-observability A skill that sets conventions for Go service telemetry: structured logs with log/slog, OpenTelemetry traces and metrics, SLO alerts. Useful once a service runs in production and you need to see what it does. CX Codex +1 ★ 83repo stars go-context A skill that makes your coding agent handle context.Context in Go correctly: cancellation, timeouts, values and the usual anti-patterns. CX Codex +1 ★ 83repo stars go-project-layout Picks a folder layout for a new Go project: cmd/, internal/, or a flat root for small tools. Useful at the start, before the structure hardens. CX Codex +1 ★ 83repo stars go-grpc A skill that steers your coding agent through gRPC work in Go: proto contracts, error codes, deadlines, interceptors and streaming. CX Codex +1 ★ 83repo stars go-dependency-injection A skill that steers your agent toward constructor injection and a single composition root in main. Useful once globals creep in or tests get hard to write. CX Codex +1 ★ 83repo stars

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor