agentsclimarketplace

Go error handling

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

26 production-grade Go skills for Claude Code, Gemini CLI, and opencode.

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 author says it does

Copied from the file, not written here

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.

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

7.0 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

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.