Go error handling
Use when writing Go code that returns, wraps, or handles errors — choosing between sentinel errors, custom types, and fmt.Errorf (%w vs %v), structuring error flow, or deciding whether to log or return. Also use when propagating errors across package boundaries or using errors.Is/As, even if the user doesn't ask about error strategy. Does not cover panic/recover patterns (see go-defensive).From its SKILL.md
npx -y skills add cxuu/golang-skills --skill go-error-handlingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- runs commandsInstructs the agent to run 2 commands, including `bash scripts/check-errors.sh` and 1 more.
SKILL.md
5.9 KB, ~1.4k tokens by cl100k_base, as published. Nobody here has run it
Go Error Handling
Compatibility:
errors.Is,errors.As, and%wwrapping require Go 1.13+; structured logging examples may uselog/slogfrom Go 1.21+.
Resource Routing
scripts/check-errors.sh- Run when checking string-based error matching, bare error propagation, and log-and-return patterns.scripts/check-errors-ast.go- Implementation helper invoked bycheck-errors.sh; patch this when changing error-flow analysis behavior.references/ERROR-FLOW.md- Read when deciding where to handle, wrap, log, or return errors.references/ERROR-TYPES.md- Read when choosing sentinel errors, typed errors, or opaque errors.references/WRAPPING.md- Read when choosing%wversus%vor crossing package boundaries.
In Go, errors are values — they are created by code and consumed by code.
Choosing an Error Strategy
- System boundary (RPC, IPC, storage)? → Wrap with
%vto avoid leaking internals - Caller needs to match specific conditions? → Sentinel or typed error, wrap with
%w - Caller just needs debugging context? →
fmt.Errorf("...: %w", err) - Leaf function, no wrapping needed? → Return the error directly
Default: wrap with %w and place it at the end of the format string.
Core Rules
Never Return Concrete Error Types
Never return concrete error types from exported functions — a concrete nil
pointer can become a non-nil interface:
// Bad: Concrete type can cause subtle bugs
func Bad() *os.PathError { /*...*/ }
// Good: Always return the error interface
func Good() error { /*...*/ }
Error Strings
Error strings should not be capitalized and should not end with punctuation. Exception: exported names, proper nouns, or acronyms.
// Bad
err := fmt.Errorf("Something bad happened.")
// Good
err := fmt.Errorf("something bad happened")
For displayed messages (logs, test failures, API responses), capitalization is appropriate.
Return Values on Error
When a function returns an error, callers must treat all non-error return values as unspecified unless explicitly documented.
Tip: Functions taking a context.Context should usually return an error
so callers can determine if the context was cancelled.
Handling Errors
When encountering an error, make a deliberate choice — do not discard
with _:
- Handle immediately — address the error and continue
- Return to caller — optionally wrapped with context
- In exceptional cases —
log.Fatalorpanic
To intentionally ignore: add a comment explaining why.
n, _ := b.Write(p) // never returns a non-nil error
For related concurrent operations, use
errgroup:
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return task1(ctx) })
g.Go(func() error { return task2(ctx) })
if err := g.Wait(); err != nil { return err }
Avoid In-Band Errors
Don't return -1, nil, or empty string to signal errors. Use multiple
returns:
// Bad: In-band error value
func Lookup(key string) int // returns -1 for missing
// Good: Explicit error or ok value
func Lookup(key string) (string, bool)
This prevents callers from writing Parse(Lookup(key)) — it causes a
compile-time error since Lookup(key) has 2 outputs.
Error Flow
Handle errors before normal code. Early returns keep the happy path unindented:
// Good: Error first, normal code unindented
if err != nil {
return err
}
// normal code
Handle errors once — either log or return, never both:
Error encountered?
├─ Caller can act on it? → Return (with context via %w)
├─ Top of call chain? → Log and handle
└─ Neither? → Log at appropriate level, continue
Error Types
Advisory: Recommended best practice.
| Caller needs to match? | Message type | Use |
|---|---|---|
| No | static | errors.New("message") |
| No | dynamic | fmt.Errorf("msg: %v", val) |
| Yes | static | var ErrFoo = errors.New("...") |
| Yes | dynamic | custom error type |
Default: Wrap with fmt.Errorf("...: %w", err). Escalate to sentinels for
errors.Is(), to custom types for errors.As().
Error Wrapping
Advisory: Recommended best practice.
- Use
%v: At system boundaries, for logging, to hide internal details - Use
%w: To preserve error chain forerrors.Is/errors.As
Key rules: Place %w at the end. Add context callers don't have. If
annotation adds nothing, return err directly.
Validation: After implementing error handling, run
bash scripts/check-errors.shto detect common anti-patterns. Then rungo vet ./...to catch additional issues.
Related Skills
- Error naming: See go-naming when naming sentinel errors (
ErrFoo) or custom error types - Testing errors: See go-testing when testing error semantics with
errors.Is/errors.Asor writing error-checking helpers - Panic handling: See go-defensive when deciding between panic and error returns, or writing recover guards
- Guard clauses: See go-control-flow when structuring early-return error flow or reducing nesting
- Logging decisions: See go-logging when choosing log levels, configuring structured logging, or deciding what context to include in log messages
What ships with it: 5 files
23.2 KB alongside SKILL.md, 1 of them executable
references/
- ERROR-FLOW.md4.0 KB
- ERROR-TYPES.md3.5 KB
- WRAPPING.md4.9 KB
scripts/
- check-errors-ast.go9.5 KB
- check-errors.shruns1.3 KB
Gives 0 of the 12 instructions most error diagnosis skills give in ~1.4k tokens
Counted across 135 of the 162 authors here whose files we hold, read 2026-09-06
- Handle, re-throw, or log in every catch blockin 12 of 135, across 7 files
- Use typed error classes over string messagesin 11 of 135, across 6 files
- Log full error context server-sidein 10 of 135, across 5 files
- Document every error code clients may receivein 9 of 135, across 4 files
- Surface errors at the boundary where they occurin 9 of 135, across 4 files
- Wrap React components in an ErrorBoundaryin 9 of 135, across 4 files
- Wrap errors with context, never lose the originalin 9 of 135, across 4 files
- Use the standard error envelope for API responsesin 9 of 135, across 4 files
- Retry only retriable errors, never 4xx client errorsin 8 of 135, across 3 files
- Retry transient failures with exponential backoff and jitterin 8 of 135
- Show users friendly messages without technical detailsin 7 of 135, across 3 files
- Use the Result pattern for expected failuresin 7 of 135, across 5 files
Said here and by no other author read
- Use %w to preserve the error chain for errors.Is/As
- Add context callers don't have when wrapping
- Return err directly when wrapping adds nothing
- Handle, return, or deliberately ignore every error
- Comment when intentionally ignoring an error
- Handle errors before normal code with early returns
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.