| name | golang-error-handling |
| description | Go error handling patterns: %w wrapping, errors.Is/As, sentinel errors, custom error types, panic prohibition rules, and error API design. Use when writing or reviewing error handling code.
|
| allowed-tools | Bash(go vet *), Bash(golangci-lint *), Bash(grep *), Read, Grep |
Go Error Handling Patterns
Go's explicit error handling is its greatest strength for reliability — when done correctly.
This skill enforces idiomatic, chain-preserving, API-stable error handling.
1. The Golden Rules
| Rule | Why |
|---|
Always use %w to wrap | Preserves chain for errors.Is/errors.As |
Never use panic in libraries/services | Caller cannot recover gracefully |
Use errors.Is not == for comparison | Works through wrap chain |
Use errors.As not type assertion | Works through wrap chain |
| Export sentinel errors only when needed | Avoid tight coupling |
2. Error Wrapping
✅ Correct: %w preserves the chain
func loadUser(id string) (*User, error) {
user, err := db.Find(id)
if err != nil {
return nil, fmt.Errorf("loadUser %s: %w", id, err)
}
return user, nil
}
if err := loadUser("42"); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return ErrUserNotFound
}
return fmt.Errorf("handleRequest: %w", err)
}
❌ Wrong: %v breaks the chain
return nil, fmt.Errorf("loadUser: %v", err)
Convention for wrapping messages
"operation name: %w" ✅
"operation name failed: %w" ✅ (common but redundant)
"failed to do X: %w" ✅
// Chain reads bottom-up:
// "handleHTTP: loadUser 42: db.Find: connection refused"
3. Sentinel Errors
Sentinel errors signal a specific, expected condition that callers should handle.
var ErrNotFound = errors.New("not found")
var ErrPermissionDenied = errors.New("permission denied")
if errors.Is(err, ErrNotFound) {
http.Error(w, "user not found", http.StatusNotFound)
return
}
When to expose sentinels
| Expose | Don't expose |
|---|
| Caller needs to branch on it | Only logged and returned |
| Different HTTP status per error | Internal implementation detail |
| Part of documented public API | Transient/infrastructure errors |
var errRetryExceeded = errors.New("max retries exceeded")
var ErrInvalidInput = errors.New("invalid input")
4. Custom Error Types
Use custom types when callers need structured data from the error.
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: field %q: %s", e.Field, e.Message)
}
return nil, &ValidationError{Field: "email", Message: "must contain @"}
var ve *ValidationError
if errors.As(err, &ve) {
http.Error(w, ve.Message, http.StatusUnprocessableEntity)
}
return nil, fmt.Errorf("createUser: %w", &ValidationError{...})
5. errors.Is and errors.As
if errors.Is(err, io.EOF) { ... }
if errors.Is(err, ErrNotFound) { ... }
var netErr *net.OpError
if errors.As(err, &netErr) {
fmt.Println(netErr.Op, netErr.Addr)
}
if err == someErr { ... }
if errors.Is(err, someErr) { ... }
6. panic Rules
func mustPositive(n int) {
if n <= 0 { panic(fmt.Sprintf("mustPositive: got %d", n)) }
}
func init() {
tmpl = template.Must(template.ParseFiles("tmpl.html"))
}
func mustMarshal(t *testing.T, v any) []byte {
t.Helper()
b, err := json.Marshal(v)
if err != nil { t.Fatalf("marshal: %v", err) }
return b
}
func GetUser(id string) *User {
u, err := db.Find(id)
if err != nil { panic(err) }
return u
}
7. Detection Commands
golangci-lint run --enable=errcheck --disable-all ./...
golangci-lint run --enable=errorlint --disable-all ./...
golangci-lint run --enable=wrapcheck --disable-all ./...
grep -rn "panic(" --include="*.go" . | grep -v "_test.go" | grep -v "//.*panic"
grep -rn "== err\b\|err ==" --include="*.go" . | grep -v "_test.go"
grep -rn 'Errorf.*%v.*err' --include="*.go" .
8. golangci-lint config for error handling
linters:
enable:
- errcheck
- errorlint
- wrapcheck
linters-settings:
errcheck:
check-type-assertions: true
check-blank: true
wrapcheck:
ignorePackageGlobs:
- "github.com/myorg/myproject/*"