Golang-skills 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).
git clone https://github.com/cxuu/golang-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/cxuu/golang-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/go-error-handling" ~/.claude/skills/cxuu-golang-skills-go-error-handling && rm -rf "$T"
skills/go-error-handling/SKILL.mdGo Error Handling
Available Scripts
— Detects error handling anti-patterns: string comparison onscripts/check-errors.sh
, bareerr.Error()
without context, and log-and-return violations. Runreturn err
for options.bash scripts/check-errors.sh --help
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
to avoid leaking internals%v - 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 —
orlog.Fatalpanic
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
Read references/ERROR-FLOW.md when structuring complex error flows, deciding between logging vs returning, implementing the handle-once pattern, or choosing structured logging levels.
Error Types
Advisory: Recommended best practice.
| Caller needs to match? | Message type | Use |
|---|---|---|
| No | static | |
| No | dynamic | |
| Yes | static | |
| Yes | dynamic | custom type |
Default: Wrap with
fmt.Errorf("...: %w", err). Escalate to sentinels for
errors.Is(), to custom types for errors.As().
Read references/ERROR-TYPES.md when defining sentinel errors, creating custom error types, or choosing error strategies for a package API.
Error Wrapping
Advisory: Recommended best practice.
- Use
: At system boundaries, for logging, to hide internal details%v - Use
: To preserve error chain for%w
/errors.Iserrors.As
Key rules: Place
%w at the end. Add context callers don't have. If
annotation adds nothing, return err directly.
Read references/WRAPPING.md when deciding between %v and %w, wrapping errors across package boundaries, or adding contextual information.
Validation: After implementing error handling, run
to detect common anti-patterns. Then runbash scripts/check-errors.shto catch additional issues.go vet ./...
Related Skills
- Error naming: See go-naming when naming sentinel errors (
) or custom error typesErrFoo - Testing errors: See go-testing when testing error semantics with
/errors.Is
or writing error-checking helperserrors.As - 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