Go Error Handling
Errors are values, not exceptions. They flow through return values, are inspected with standard tools, and always carry context about what went wrong.
Error Strategy Decision Table
| Situation |
Strategy |
Example |
| Expected failure (file not found, bad input) |
Return error |
return fmt.Errorf("open %s: %w", path, err) |
| Caller needs to distinguish error kinds |
Sentinel or custom type |
var ErrNotFound = errors.New("not found") |
| Adding context (see Wrapping vs Formatting) |
Wrap %w / format %v |
fmt.Errorf("loading config: %w", err) |
| Truly unrecoverable (programmer bug) |
panic |
Nil map write, index out of bounds |
| Library boundary cleanup |
recover in deferred func |
HTTP middleware, plugin host |
The Error Interface
type error interface {
Error() string
}
Any type implementing Error() string is an error. This simplicity is the entire design.
Wrapping vs Formatting
| Verb |
Preserves chain? |
errors.Is/As work? |
Use when |
%w |
Yes |
Yes |
Caller may need to match the cause |
%v |
No |
No |
Adding context, hiding implementation details |
Rule: Wrap (%w) by default. Format (%v) only when you explicitly want to hide the cause from callers (e.g., at package boundaries).
Custom Error Types
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
// Usage
func validateAge(age int) error {
if age < 0 {
return &ValidationError{Field: "age", Message: "must be non-negative"}
}
return nil
}
// Caller inspects with errors.As
var ve *ValidationError
if errors.As(err, &ve) {
log.Printf("field %s: %s", ve.Field, ve.Message)
}
Anti-patterns
| Anti-pattern |
Problem |
Fix |
_ = doSomething() |
Silent failure |
Handle every error, or add //nolint with justification |
panic for expected failures |
Crashes the program |
Return error — panic is for programmer bugs only |
return err without context |
Error message is cryptic at the top of the chain |
return fmt.Errorf("doing X: %w", err) |
Stuttering: "failed to open file: open /x: no such file" |
Redundant prefixes |
Add context about your operation, not the callee's |
| Comparing error strings |
Fragile, breaks on reword |
Use errors.Is for sentinel errors, errors.As for types |
Read On Demand
| Read When |
File |
| Full patterns: sentinel errors, custom types, wrapping chains, panic/recover, multi-error |
Error Patterns |
Benchmark
Scenario: .benchmarks/scenarios/golang-error-handling-001-wrap-vs-format.md
| Model |
Without |
With |
Delta |
| claude-opus-4-8 |
100% |
100% |
+0% |
| claude-sonnet-4-6 |
83% |
100% |
+17% |
| claude-haiku-4-5 |
50% |
83% |
+33% |
PASS (run 2026-06-25). Strong lift on weak models (haiku +33, sonnet +17); opus saturated. Skill flips %v→%w and the panic→returned-error fix that baselines under-apply. Gate per .agents/skills/skill-optimizer/rules/release-gates.md.
1---2name: error-handling3description: Go error handling — error interface, custom error types, wrapping, sentinel errors, errors.Is/As, panic/recover. TRIGGER when: user asks about Go error handling, if err != nil, custom errors, error wrapping, fmt.Errorf %w, errors.Is, errors.As, sentinel errors, panic recover, when to panic in Go, error types in Go, Go error best practices, Go error propagation, error chains, Go error interface, handling errors in Go, Go error patterns. DO NOT USE when: user needs general error-handling philosophy outside Go, or panic/recover questions unrelated to Go's error interface.4---56# Go Error Handling78Errors are values, not exceptions. They flow through return values, are inspected with standard tools, and always carry context about what went wrong.910---1112## Error Strategy Decision Table1314| Situation | Strategy | Example |15| -------------------------------------------- | -------------------------- | --------------------------------------------- |16| Expected failure (file not found, bad input) | Return `error` | `return fmt.Errorf("open %s: %w", path, err)` |17| Caller needs to distinguish error kinds | Sentinel or custom type | `var ErrNotFound = errors.New("not found")` |18| Adding context (see Wrapping vs Formatting) | Wrap `%w` / format `%v` | `fmt.Errorf("loading config: %w", err)` |19| Truly unrecoverable (programmer bug) | `panic` | Nil map write, index out of bounds |20| Library boundary cleanup | `recover` in deferred func | HTTP middleware, plugin host |2122---2324## The Error Interface2526```go27type error interface {28 Error() string29}30```3132Any type implementing `Error() string` is an error. This simplicity is the entire design.3334---3536## Wrapping vs Formatting3738| Verb | Preserves chain? | `errors.Is`/`As` work? | Use when |39| ---- | ---------------- | ---------------------- | --------------------------------------------- |40| `%w` | Yes | Yes | Caller may need to match the cause |41| `%v` | No | No | Adding context, hiding implementation details |4243**Rule:** Wrap (`%w`) by default. Format (`%v`) only when you explicitly want to hide the cause from callers (e.g., at package boundaries).4445---4647## Custom Error Types4849```go50type ValidationError struct {51 Field string52 Message string53}5455func (e *ValidationError) Error() string {56 return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)57}5859// Usage60func validateAge(age int) error {61 if age < 0 {62 return &ValidationError{Field: "age", Message: "must be non-negative"}63 }64 return nil65}6667// Caller inspects with errors.As68var ve *ValidationError69if errors.As(err, &ve) {70 log.Printf("field %s: %s", ve.Field, ve.Message)71}72```7374---7576## Anti-patterns7778| Anti-pattern | Problem | Fix |79| ---------------------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------- |80| `_ = doSomething()` | Silent failure | Handle every error, or add `//nolint` with justification |81| `panic` for expected failures | Crashes the program | Return `error` — panic is for programmer bugs only |82| `return err` without context | Error message is cryptic at the top of the chain | `return fmt.Errorf("doing X: %w", err)` |83| Stuttering: `"failed to open file: open /x: no such file"` | Redundant prefixes | Add context about _your_ operation, not the callee's |84| Comparing error strings | Fragile, breaks on reword | Use `errors.Is` for sentinel errors, `errors.As` for types |8586---8788## Read On Demand8990| Read When | File |91| ----------------------------------------------------------------------------------------- | ---------------------------------------------- |92| Full patterns: sentinel errors, custom types, wrapping chains, panic/recover, multi-error | [Error Patterns](references/error-patterns.md) |9394---9596## Benchmark9798Scenario: `.benchmarks/scenarios/golang-error-handling-001-wrap-vs-format.md`99100| Model | Without | With | Delta |101| ----------------- | ------- | ---- | ----- |102| claude-opus-4-8 | 100% | 100% | +0% |103| claude-sonnet-4-6 | 83% | 100% | +17% |104| claude-haiku-4-5 | 50% | 83% | +33% |105106> **PASS** (run 2026-06-25). Strong lift on weak models (haiku +33, sonnet +17); opus saturated. Skill flips `%v`→`%w` and the `panic`→returned-error fix that baselines under-apply. Gate per `.agents/skills/skill-optimizer/rules/release-gates.md`.