---
name: idiomatic-go-error-handling
source: https://app.decimal.ai/s/idiomatic-go-error-handling@1/SKILL.md
source_sha256: ce6a7483dc1a
---

# Idiomatic Go error handling

## Contract

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.

## Rules

1. **Wrap with `%w`, never `%v` / `%s` / concatenation.** When you return an error that came
   out 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.

2. **Error strings are lowercase with no trailing punctuation.** Every message — the part
   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.

3. **Context reads `operation: %w`.** Put the added context BEFORE the wrapped error, joined
   with 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"`.

4. **Sentinel errors: `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`).

5. **Error TYPES take the `Error` SUFFIX and implement `Error() string`.** A custom error
   that 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`).

6. **Match sentinels with `errors.Is`, never `==`.** To test whether an error is or wraps a
   sentinel, use `errors.Is(err, ErrX)`. `err == ErrX` only matches an unwrapped error and
   silently misses a wrapped one.

7. **Extract typed errors with `errors.As`, never a type assertion.** To pull out a typed
   error 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.

## Worked examples

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) }
```

## Edge cases & exceptions

- **Leading proper noun / initialism stays capitalized:** `errors.New("EOF while reading")`,
  `fmt.Errorf("JSON decode: %w", err)` are fine — the lowercase rule exempts proper nouns and
  acronyms only.
- **Don't wrap when you add no context** — returning the bare `err` is correct; reach for
  `fmt.Errorf(... %w ...)` only when you have real context to add. Never wrap the same error
  twice.
- **One cause per wrap** — put a single `%w` in a given `fmt.Errorf`; wrap one underlying
  error, not several.
- **A `nil` error stays `nil`** — never wrap unconditionally; guard with `if err != nil`.
- **Sentinel vs type:** if callers only need to know *which* error → a sentinel VALUE (`Err`
  prefix, matched with `errors.Is`). If they need DATA off the error → an error TYPE (`Error`
  suffix, matched with `errors.As`).

## Do / Don't

- DON'T wrap with `%v` or `"..." + err.Error()`. ALWAYS wrap with `%w` so the chain survives.
- DON'T capitalize or end an error string with a period. ALWAYS lowercase, no trailing punctuation.
- DON'T name a sentinel `NotFoundError` / `notFound`. ALWAYS `ErrNotFound`, built with `errors.New`.
- DON'T give an error TYPE the `Err` prefix. ALWAYS the `Error` suffix with an `Error() string` method.
- DON'T compare with `err == ErrX`. ALWAYS `errors.Is(err, ErrX)`.
- DON'T type-assert `err.(*T)`. ALWAYS `errors.As(err, &target)`.

## Common mistakes (the base model's wrong defaults)

- Wrapping with `%v` (or `fmt.Errorf("...", err.Error())`), which breaks `errors.Is` / `errors.As` downstream.
- Capitalized, period-terminated messages: `"Failed to load config."`.
- Building a sentinel with `fmt.Errorf`, or naming it with an `Error` suffix instead of the `Err` prefix.
- Giving an error TYPE the `Err` prefix, or implementing `Msg()` / `String()` instead of `Error() string`.
- Comparing errors with `==` and type-asserting with `.(*T)` instead of `errors.Is` / `errors.As`.

## Quick checklist

- [ ] Wrapped errors use `%w` (not `%v` / `%s` / concatenation).
- [ ] Error strings lowercase, no trailing punctuation.
- [ ] Context is `operation: %w` (colon + space before `%w`).
- [ ] Sentinels: `Err`-prefixed package vars via `errors.New`.
- [ ] Error types: `Error` suffix + `Error() string`.
- [ ] Sentinel checks use `errors.Is`; typed extraction uses `errors.As`.
