agentleFS
h0rn3th0rn3t/golang-skills/skills/go-error-handling/SKILL.md

go-error-handling

Skill7 starsChanged 9 days agoApache-2.0View on GitHub

Use when returning, wrapping, or handling Go errors; choosing sentinels or custom errors; using errors.Is/As or %w; or deciding whether to log or return. Panic/recover belongs to go-defensive.

3 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

7 rules. 0 are common, 1 is distinctive, and 6 are very specific to this project.

  • 0

    Common

    Most of the 3 edited versions we read keep these.

  • 1

    Distinctive

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Add a custom type when callers need new programmatic data or behavior beyond the existing cause and a contextual message.

    line 70
  2. Very specific:

    Use errors.AsType[T] (Go 1.26+) — it returns the value instead of writing through a pointer.

    line 78
  3. Very specific:

    Keep the original err visible to every check.

    line 84
  4. Very specific:

    Never name the extracted cause err: in an else if chain the next check receives the typed nil, and AsType panics unwrapping it.

    line 95
  5. Very specific:

    Use %w: When the underlying cause is part of the caller-facing contract

    line 104
  6. Very specific:

    Key rules: Place %w at the end (two causes of one failure take two %w verbs in one fmt.Errorf).

    line 106
  7. Very specific:

    If annotation adds nothing, return err directly.

    line 111

“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Go Error Handling

Compatibility: Baseline Go 1.27 (see COMPATIBILITY.md). errors.AsType[T] requires Go 1.26+; errors.Is/errors.As and %w work on every supported release.

Resource Routing

  • scripts/check-errors.sh - Run when checking string-based error matching and log-and-return patterns; --bare-return adds the opt-in bare return err review.
  • scripts/check-errors-ast.go - Implementation helper built and run by check-errors.sh; not run or read directly.

Error Flow

Handle errors first and return, so the success path stays unindented; go-style-core owns nesting, if-init scope, and else.

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 (main, a request handler)? → Log the detail and handle it
│   A handler writes a status, never the error text; that is the one place
│   the same error is both logged and answered.
└─ Neither? → Log at appropriate level, continue

Handling one matched case — errors.Is(err, ErrUserNotFound) falls back to a default — and returning the rest wrapped is handling once too.

An error discarded on purpose says why on the same line (n, _ := b.Write(p) // never returns a non-nil error); a bare _ is a finding. Independent failures that must all be reported — validating several fields, closing several resources — aggregate with errors.Join, which errors.Is and errors.AsType see through:

return errors.Join(closeErr, flushErr)

Related concurrent work returns through errgroup.WithContext when go.mod already requires golang.org/x/sync: the first failure cancels the rest and is what Wait returns. Without it, wg.Go with context.WithCancelCause and errors.Join covers the same.


Error Types

Caller contractUse
Preserve an existing cause while adding contextfmt.Errorf("...: %w", err)
Match a stable condition without additional dataReuse a suitable sentinel; define one for a new condition
Inspect additional structured fields or type-specific behaviorCustom error type
Message only, no stable matching contracterrors.New for static text; fmt.Errorf for dynamic text

A failure is never an in-band value (-1, "", nil): return (T, error) or (T, bool). Dynamic wording alone does not justify a new type. %w already preserves existing sentinels and typed causes for errors.Is and errors.AsType. Add a custom type when callers need new programmatic data or behavior beyond the existing cause and a contextual message. Whatever the type, the result is declared as error: a concrete *PathError result turns a nil pointer into a non-nil interface (go-defensive owns the mechanism).

Matching a typed error

Use errors.AsType[T] (Go 1.26+) — it returns the value instead of writing through a pointer. go fix -errorsastype ./... (Go 1.27 toolchain) rewrites the old form.

Matching multiple typed errors

Keep the original err visible to every check; name each extracted cause:

if pathErr, ok := errors.AsType[*fs.PathError](err); ok {
    return pathErr.Path
}
if linkErr, ok := errors.AsType[*os.LinkError](err); ok {
    return linkErr.New
}

Never name the extracted cause err: in an else if chain the next check receives the typed nil, and AsType panics unwrapping it.


Error Wrapping

  • Use %v: For display or annotation that deliberately omits the error chain; it keeps the text, not the identity, and redacts nothing
  • Use %w: When the underlying cause is part of the caller-facing contract

Key rules: Place %w at the end (two causes of one failure take two %w verbs in one fmt.Errorf). Add context callers don't have: an os error already names its path, so launch codes unavailable: %w rather than read settings.txt: %w. Context names the operation — load config: %w, not failed to load config: %w: each layer that says "failed to" repeats it. If annotation adds nothing, return err directly. One error often serves two audiences — the operator reading a log line and the caller matching with errors.Is — and fmt.Errorf("resolve %q: %w", sku, err) serves both where %v serves only the first.

Validation: From the project, run bash <installed-skill-dir>/scripts/check-errors.sh ./... to detect common anti-patterns. The go-linting gate covers the rest and runs once, at the end of the task, not again here.


Related Skills

  • go-naming: ErrX sentinels and XError types.
  • go-testing: errors.Is/errors.AsType under test, error-checking helpers.
  • go-defensive: panic versus error, recover guards.
  • go-logging: log levels and what a log line carries.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.