agentsclimarketplace

Go error handling

Skill ctoth/golang-skills-plugin/plugins/golang/skills/go-error-handling

Agent skill plugins for Go code quality and performance work.

Install
npx -y skills add ctoth/golang-skills-plugin --skill go-error-handling

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

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."

SKILL.md

13.2 KB, as published. Nobody here has run it

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).

// WRONG — %v flattens err to a string; errors.Is/As downstream can never match
return fmt.Errorf("decompress %s: %v", name, err)

// RIGHT — %w keeps err in the chain for errors.Is / errors.As
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).

// WRONG — string match: brittle, breaks the moment the message is wrapped or reworded
if err.Error() == "not found" { ... }

// RIGHT — match the value through the whole chain
if errors.Is(err, ErrNotFound) { ... }

// RIGHT — extract a typed error and read its fields
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).

// Sentinel — caller only needs to know "which" error
var ErrNotFound = errors.New("not found")

// Custom type — caller needs data off the error
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:

// RIGHT — %w: the wrapped error IS part of this function's contract
return fmt.Errorf("loading user %d: %w", id, ErrNotFound)

// RIGHT — %v: deliberately hide an internal type so it can't become API
return fmt.Errorf("accessing DB: %v", err) // caller cannot reach sql.ErrNoRows

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...) // nil if all validations passed

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).

// WRONG — capitalized, punctuated, and a "failed to" chain that stacks badly
//   failed to x: failed to y: Failed to create new store.
return fmt.Errorf("Failed to create new store: %w", err)

// RIGHT — lowercase, no period, terse; chains read as "x: y: new store: <cause>"
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).

// WRONG — logged here AND returned: every layer logs the same failure
u, err := getUser(id)
if err != nil {
	log.Printf("could not get user %q: %v", id, err)
	return err
}

// RIGHT — wrap and return; let the caller decide whether to log
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() {
		// surface the Close error only if the body didn't already fail
		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-recoverdefer 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-analysiserrorlint (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

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.