| name | go-error-handling |
| description | Guides Go error handling as values — wrap with fmt.Errorf and %w to preserve the chain, inspect with errors.Is (sentinel) and errors.As / errors.AsType (typed) instead of string-matching, choose sentinel vs custom error types and when they become API, combine with errors.Join, write lowercase un-punctuated messages, handle each error exactly once, and decorate or close-with-error via named returns and defer. Auto-invokes when writing or editing error returns, fmt.Errorf, errors.Is/As, custom error types, or on "handle this error" / "why is this error not matching" requests. The depth behind the policy root's "errors are values, never silently discarded." |
| allowed-tools | Read, Glob, Grep |
| compatibility | Claude Code, Codex CLI, Gemini CLI |
Go Error Handling
"The key lesson, however, is that errors are values and the full power of the Go programming language is available for processing them."
— Errors are values (Rob Pike)
"In Go 1.13, the fmt.Errorf function supports a new %w verb ... wrapping an error makes that error part of your API. If you don't want to commit to supporting that error as part of your API in the future, you shouldn't wrap the error."
— Working with Errors in Go 1.13
An error in Go is an ordinary value, not a thrown exception. That single fact decides everything below: you wrap values, you inspect values, you compare values — you never re-parse a rendered string. This skill owns the depth behind go-idiomatic-discipline's headline floor ("errors are values, never silently discarded"); the floor — always check the error — is assumed here.
1. Wrap With %w to Preserve the Chain
When you add context as an error travels up the stack, use fmt.Errorf with %w. It returns an error whose Unwrap method returns the wrapped error, so the whole chain stays inspectable: "the error returned by fmt.Errorf will have an Unwrap method returning the argument of %w ... In all other ways, %w is identical to %v" (Go 1.13 errors).
return fmt.Errorf("decompress %s: %v", name, err)
return fmt.Errorf("decompress %s: %w", name, err)
Place %w at the end of the message: "Prefer to place %w at the end of an error string" (Google Style Guide — best-practices). Each wrap "adds a new entry to the front of the error chain."
2. Inspect With errors.Is and errors.As — Never String-Match
errors.Is "reports whether any error in err's tree matches target"; errors.As "finds the first error in err's tree that matches target, and ... sets target to that error value" (pkg.go.dev/errors). Both walk the %w chain, so a deeply wrapped sentinel still matches. "In the simplest case, the errors.Is function behaves like a comparison to a sentinel error, and the errors.As function behaves like a type assertion. When operating on wrapped errors, however, these functions consider all the errors in a chain" (Go 1.13 errors).
if err.Error() == "not found" { ... }
if errors.Is(err, ErrNotFound) { ... }
var ve *ValidationError
if errors.As(err, &ve) {
log.Printf("bad field: %s", ve.Field)
}
errors.As "panics if target is not a non-nil pointer to either a type that implements error, or to any interface type" (pkg.go.dev/errors) — the target is always &someErr, never someErr.
Go 1.26: errors.AsType[E error](err error) (E, bool) is "a generic version of As. It is type-safe, faster, and, in most cases, easier to use" (Go 1.26 release notes). Prefer it when your module is on 1.26+:
if pathErr, ok := errors.AsType[*fs.PathError](err); ok {
log.Printf("failed at path: %s", pathErr.Path)
}
3. Sentinel Errors vs Custom Types
A sentinel is a fixed value matched with errors.Is: var ErrNotFound = errors.New("not found"). A custom type carries structured data and is matched with errors.As. Choose by what the caller needs: "For an error with a dynamic string, use fmt.Errorf if the caller does not need to match it, and a custom error if the caller does need to match it" (Uber Go Style Guide).
var ErrNotFound = errors.New("not found")
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validate %s: %s", e.Field, e.Msg)
}
If the caller does not need to interrogate the error, give it no structure at all. If it does, "give the error value structure so that this can be done programmatically rather than having the caller perform string matching" (Google Style Guide).
Both become API. "Note that if you export error variables or types from a package, they will become part of the public API of the package" (Uber). Export sentinels and types sparingly — only when callers genuinely branch on them — and document them, so callers can anticipate what they may handle.
4. The Wrap-vs-Don't-Wrap Decision
"Wrap an error to expose it to callers. Do not wrap an error when doing so would expose implementation details" (Go 1.13 errors). The canonical trap: a function that internally uses database/sql and lets sql.ErrNoRows leak through %w has now promised that error forever — "wrapping an error makes that error part of your API." Use %v at that boundary to deliberately obfuscate the cause:
return fmt.Errorf("loading user %d: %w", id, ErrNotFound)
return fmt.Errorf("accessing DB: %v", err)
Uber frames the same %w/%v fork: "Use %w if the caller should have access to the underlying error ... Use %v to obfuscate the underlying error. Callers will be unable to match it, but you can switch to %w in the future if needed" (Uber). %w is the good default; %v is the conscious choice to keep an option open.
5. errors.Join for Multiple Errors
When several independent operations can each fail (validating many fields, closing many resources), collect them. errors.Join (Go 1.20) "returns an error that wraps the given errors. Any nil error values are discarded. Join returns nil if every value in errs is nil" (pkg.go.dev/errors). The result implements Unwrap() []error, and errors.Is/As traverse every branch.
var errs []error
for _, f := range fields {
if err := validate(f); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
6. Message Conventions
"Error strings should not be capitalized (unless beginning with proper nouns or acronyms) or end with punctuation, since they are usually printed following other context" (CodeReviewComments — Error Strings). And keep context succinct: drop "failed to", which "state[s] the obvious and pile[s] up as the error percolates up through the stack" (Uber).
return fmt.Errorf("Failed to create new store: %w", err)
return fmt.Errorf("new store: %w", err)
Name sentinels with an Err/err prefix and custom types with an Error suffix (Uber) — go-naming-and-style owns the full naming rule.
7. Handle Each Error Once
An error is handled once: match it, recover from it, return it (wrapped or bare) — but not two of those. The most common double-handling is log-and-return: "If you return an error, it's usually better not to log it yourself but rather let the caller handle it" (Google Style Guide); doing both "makes a lot of noise in the application logs for little value" (Uber).
u, err := getUser(id)
if err != nil {
log.Printf("could not get user %q: %v", id, err)
return err
}
u, err := getUser(id)
if err != nil {
return fmt.Errorf("get user %q: %w", id, err)
}
Logging and degrading gracefully (not returning) is fine; that is handling it once. The log side of this rule is owned by go-slog-logging.
8. Line of Sight and Defer-Based Decoration
Handle the error first and return, keeping the happy path flat: "keep the normal code path at a minimal indentation, and indent the error handling, dealing with it first" (CodeReviewComments — Indent Error Flow). To add context on every return path, or to surface a Close error, use a named return plus defer:
func writeFile(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("creating %s: %w", path, err)
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = fmt.Errorf("closing %s: %w", path, cerr)
}
}()
if _, err := f.Write(data); err != nil {
return fmt.Errorf("writing %s: %w", path, err)
}
return nil
}
A bare defer f.Close() silently drops a flush/close error on a file you wrote — for writers, capture it. This skill owns the error-decoration idiom; the defer mechanics it relies on (LIFO, argument evaluation timing, loop-defer) belong to go-defer-panic-recover.
9. Don't panic for Ordinary Errors
A failed lookup, bad input, or missing file is a value to return, not a process to crash. "Don't use panic for normal error handling. Use error and multiple return values" — routed in full to go-defer-panic-recover. The typed-nil trap (returning *MyError-typed nil as an error, which is never nil) is an interface-representation fact owned by go-interfaces; declare returns as error, not *MyError.
10. Who Suffers When Errors Are Mishandled
A mishandled error never hurts the author at write time — it hurts someone later:
- The on-call engineer staring at a
500 with a log line that reads only error: EOF — because the error was returned bare, with no %w context naming which call to which dependency failed. The wrap they didn't write is the breadcrumb trail they don't have at 3am.
- The teammate who loses an afternoon to a bug whose root cause was a discarded
_ = json.Unmarshal(...) three layers down: the malformed payload became a silent zero-value struct, and the failure surfaced somewhere unrelated. "Whatever you do, always check your errors!" (Errors are values) is written for that teammate.
- The next maintainer who can't tell whether
if err == ErrNotFound is safe, because somewhere upstream a caller wrapped that sentinel with %w and the bare == now silently never matches — the bug §2 exists to prevent.
Good error handling is empathy expressed in code: the context you add, the sentinel you expose with errors.Is, and the error you don't swallow are all gifts to whoever debugs this under load.
11. Routing to Related Skills
go-idiomatic-discipline — the policy root; this skill is the depth behind its "errors are values" floor.
go-defer-panic-recover — defer mechanics behind §8, and when panic/recover are legitimate.
go-interfaces — the typed-nil error gotcha; why a function returns error, not a concrete pointer type.
go-naming-and-style — the Err prefix / Error suffix naming rule and error-string casing.
go-slog-logging — the log side of "handle once" (§7).
go-tooling-and-static-analysis — errorlint (catches %v-instead-of-%w and == comparisons) and errcheck (catches discarded errors) in CI.
12. Reference Files
High-frequency error-handling anti-patterns in LLM-generated Go, each with wrong/right code and citations:
${CLAUDE_SKILL_DIR}/references/common-mistakes.md
Source provenance for every claim in this skill:
${CLAUDE_SKILL_DIR}/references/sources.yaml