Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when writing Go code that returns, defines, or inspects an error: apply the Go 1.13 error idioms (wrap with %w, Err-prefixed sentinels via errors.New, Error-suffixed types, errors.Is / errors.As) and Go error-string style — which cheaper models do not do by default.
.claude/skills/idiomatic-go-error-handling/SKILL.md| Model | Lift | Δ tokens | Δ turns | Cases | Verified |
|---|---|---|---|---|---|
| gemini-3.6-flashbest | +18% | +111% | 0% | 22 | 54d ago |
| gemini-3.5-flash | pending re-run | — | |||
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-16 | ✗→✓ | ▲ Improved | — | — |
| case-21 | ✗→✓ | ▲ Improved | — | — |
| case-11 | ✗→✓ | ▲ Improved | — | — |
| case-08 | ✓→✓ | = Same ✓ | — | — |
Enforces the Go 1.13 error idioms and Go error-string style on every Go snippet that returns, defines, or inspects an error. Apply whenever you write Go code that can fail: returning an error from a function, declaring error values or types, or branching on which error occurred.
%w, never %v / %s / concatenation. When you return an error that cameout of a called function and want to add context, wrap it: fmt.Errorf("<context>: %w", err). The %w verb (Go 1.13) keeps the original error in the chain so callers can still match it with errors.Is / errors.As. %v, %s, and "..." + err.Error() flatten the chain and destroy that.
before : %w, and every errors.New / Error() string — starts lowercase (unless it begins with a proper noun or an initialism like HTTP, JSON, URL, TLS) and ends with no period, no exclamation mark, no trailing colon. Errors are usually wrapped by callers, so a capitalized / punctuated string reads as send email: Failed to dial.: mid sentence.
operation: %w. Put the added context BEFORE the wrapped error, joinedwith a literal ": " (colon then a single space) so the format string ends with : %w: fmt.Errorf("open config %s: %w", path, err). Name the operation that failed; don't restate "error" / "failed to".
Err-prefixed package vars built with errors.New. A fixed,comparable error condition is a package-level variable named with the Err prefix (ErrNotFound, ErrTimeout), created with errors.New("...") — not fmt.Errorf, not a struct literal, and not an Error-suffixed or unprefixed name. Unexported sentinels use the lowercase err prefix (errClosed).
Error SUFFIX and implement Error() string. A custom errorthat carries data (fields, codes) is a type named with the Error suffix (ValidationError, RateLimitError) — NOT an Err prefix, which is reserved for sentinel values — with a method of exact signature Error() string (named Error, no arguments, returning string).
errors.Is, never ==. To test whether an error is or wraps asentinel, use errors.Is(err, ErrX). err == ErrX only matches an unwrapped error and silently misses a wrapped one.
errors.As, never a type assertion. To pull out a typederror and read its fields, declare var target *T and call errors.As(err, &target) — not err.(*T) and not a switch err.(type), both of which miss wrapped errors.
Each pair is BEFORE (the base model's non-idiomatic default) → AFTER (conforming).
Wrap with %w, lowercase, no period.
go// BEFORE return fmt.Errorf("Failed to send email: %v", err) // AFTER return fmt.Errorf("send email: %w", err)
Sentinel value.
go// BEFORE var NotFoundError = fmt.Errorf("Item was not found.") // AFTER var ErrNotFound = errors.New("item not found")
Error type carrying data.
go// BEFORE type ErrRateLimit struct{ RetryAfter time.Duration } func (e ErrRateLimit) Msg() string { return "Rate limited." } // AFTER type RateLimitError struct{ RetryAfter time.Duration } func (e *RateLimitError) Error() string { return fmt.Sprintf("rate limited, retry after %s", e.RetryAfter) }
Match a sentinel.
go// BEFORE if err == sql.ErrNoRows { return nil, nil } // AFTER if errors.Is(err, sql.ErrNoRows) { return nil, nil }
Extract a typed error.
go// BEFORE if pe, ok := err.(*os.PathError); ok { log.Print(pe.Path) } // AFTER var pe *os.PathError if errors.As(err, &pe) { log.Print(pe.Path) }
errors.New("EOF while reading"),fmt.Errorf("JSON decode: %w", err) are fine — the lowercase rule exempts proper nouns and acronyms only.
err is correct; reach forfmt.Errorf(... %w ...) only when you have real context to add. Never wrap the same error twice.
%w in a given fmt.Errorf; wrap one underlyingerror, not several.
nil error stays nil — never wrap unconditionally; guard with if err != nil.Errprefix, matched with errors.Is). If they need DATA off the error → an error TYPE (Error suffix, matched with errors.As).
%v or "..." + err.Error(). ALWAYS wrap with %w so the chain survives.NotFoundError / notFound. ALWAYS ErrNotFound, built with errors.New.Err prefix. ALWAYS the Error suffix with an Error() string method.err == ErrX. ALWAYS errors.Is(err, ErrX).err.(*T). ALWAYS errors.As(err, &target).%v (or fmt.Errorf("...", err.Error())), which breaks errors.Is / errors.As downstream."Failed to load config.".fmt.Errorf, or naming it with an Error suffix instead of the Err prefix.Err prefix, or implementing Msg() / String() instead of Error() string.== and type-asserting with .(*T) instead of errors.Is / errors.As.%w (not %v / %s / concatenation).operation: %w (colon + space before %w).Err-prefixed package vars via errors.New.Error suffix + Error() string.errors.Is; typed extraction uses errors.As.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.5-flash | verified | 7/9/2026 | +33% |
Other measured skills in the registry, with their headline benchmark lift.