agentsclimarketplace

Go error handling

Skill muratmirgun/gophers/skills/go-error-handling

Use when writing, wrapping, inspecting, or logging Go errors. Covers strategy choice (sentinel vs typed vs opaque), wrapping with %w/%v, errors.Is/As/Join, the log-or-return rule, error strings, and panic/recover boundaries. Apply proactively whenever a function returns or accepts an error, even if the user has not asked about error handling.From its SKILL.md

Install
npx -y skills add muratmirgun/gophers --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

  • 8 stars8 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 file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

6.8 KB, as published. Nobody here has run it

Go Error Handling

Errors in Go are values. Treat them as part of the API: choose a strategy per failure mode, propagate with wrapping, inspect with errors.Is/As, and handle each error exactly once.

Core Rules

  1. Errors are values, not exceptions. Return them; do not panic across API boundaries.
  2. Handle each error exactly once. Either log it or return it — never both.
  3. The caller decides what is exceptional. Library code returns; binaries (or top-level handlers) decide whether to log, retry, or exit.
  4. Wrap only when you add real context. A wrap that just repeats the function name is noise. Use %w to preserve identity; %v to deliberately hide an unstable type.

Strategy Decision

Pick the simplest strategy that meets the caller's needs:

StrategyWhen to useExample
Opaque error (default)Caller only needs to know something failederrors.New("invalid input")
Sentinel errorCaller needs to test for a specific named conditionio.EOF, sql.ErrNoRows
Typed errorCaller needs structured fields (path, code, retry-after)*os.PathError, *url.Error
Joined errorsA single operation produced several independent failureserrors.Join(errA, errB)

Read references/strategy-decision.md when the caller's needs are unclear or when migrating between strategies without breaking callers.

Writing Errors

Strings

  • Lowercase, no trailing punctuation. Errors are composed: fmt.Errorf("write %s: %w", path, err) reads as one sentence.
  • Be specific. "open config: permission denied" beats "failed to open file".
  • Do not include the function name. Stack context is added by wrapping at each layer.

Creating

// Opaque — the caller only checks != nil
return errors.New("invalid character in token")

// Sentinel — exported, package-level, named ErrXxx
var ErrNotFound = errors.New("user: not found")

// Typed — when callers need structured fields
type ValidationError struct {
    Field string
    Rule  string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation: %s violates %s", e.Field, e.Rule)
}

Wrapping and Inspection

Wrap with %w to add context while preserving identity

if err := db.Get(id); err != nil {
    return fmt.Errorf("loading user %d: %w", id, err)
}

Inspect with errors.Is (identity) and errors.As (type)

if errors.Is(err, sql.ErrNoRows) { /* handled */ }

var ve *ValidationError
if errors.As(err, &ve) {
    return reply.BadRequest(ve.Field)
}

Never compare error strings (err.Error() == "...") — strings are not stable API.

Join independent failures

errs := errors.Join(
    validate(name),
    validate(email),
    validate(password),
)
if errs != nil {
    return errs // errors.Is/As walks both branches
}

Read references/wrapping-vs-shadowing.md when deciding between %w (expose) and %v (hide), or when wrapping would leak an implementation detail.

Error Flow

Log or return — not both

// Bad: caller will log it again, producing duplicate lines
if err := svc.Do(ctx); err != nil {
    slog.ErrorContext(ctx, "svc.Do failed", "err", err)
    return err
}

// Good: log only at the boundary that decides the request is done
if err := svc.Do(ctx); err != nil {
    return fmt.Errorf("doing svc work: %w", err)
}

The HTTP handler / job runner / main is the only layer that logs.

Reduce nesting with guard clauses

// Bad
if err == nil {
    if x, ok := f(); ok {
        return x, nil
    }
}
return zero, err

// Good
if err != nil {
    return zero, err
}
x, ok := f()
if !ok {
    return zero, errSomething
}
return x, nil

Panic and Recover

panic is for programmer errors (impossible states) and package initialization. It is never the right way to return a normal failure.

// Acceptable: invariants the type guarantees
func (q *Queue) MustEnqueue(v T) { if err := q.Enqueue(v); err != nil { panic(err) } }

// Acceptable: recover at the goroutine boundary so one bad request cannot kill the server
defer func() {
    if r := recover(); r != nil {
        log.Error("panic recovered", "value", r, "stack", debug.Stack())
        http.Error(w, "internal error", 500)
    }
}()

Do not use recover to convert panics into errors as normal control flow.

For custom error types, implement Unwrap() error (or Unwrap() []error in Go 1.20+) so errors.Is/As can reach the cause. See references/wrapping-vs-shadowing.md.

Anti-Patterns

Anti-patternWhy it hurtsDo this instead
return errors.New(err.Error())Drops identity; errors.Is breaksreturn fmt.Errorf("ctx: %w", err)
if err.Error() == "EOF"String matching against unstable texterrors.Is(err, io.EOF)
_ = doThing()Silently swallows failuresHandle, log at boundary, or document why
Returning *MyError (concrete pointer)Typed-nil trap; non-nil interfaceReturn error (see references/typed-nil-trap.md)
Logging then returning the same errorDuplicate log lines, no single source of truthLog only at the top boundary
Wrapping at every layer with no new info"a: b: c: d: real error" chainsDrop the wrap and return err

Verification Checklist

Before finishing an error-handling change:

  • No err.Error() string comparisons
  • All wrapping uses %w (or %v is intentional and commented)
  • Functions return the error interface, not concrete types
  • Each error is logged at most once (at the request/job boundary)
  • Sentinels are package-level var ErrXxx = errors.New(...)
  • Typed errors expose only the fields callers actually need

References

What ships with it: 3 files

7.5 KB alongside SKILL.md

Gives 0 of the 12 instructions most error diagnosis skills give

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

  • Return errors as values, never panic across API boundaries
  • Log only at the request or job boundary
  • Wrap with %w to preserve error identity
  • Make error messages specific
  • Reduce nesting with guard clauses
  • Implement Unwrap on custom error types

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once.

Keep looking

Skills are one crate of 626,194. 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.