go-error-handling
Original:🇺🇸 English
Translated
1 scripts
Go error handling patterns: wrapping with context, sentinel errors, custom error types, errors.Is/As chains, and HTTP error mapping. Use when implementing error returns, defining package-level errors, creating custom error types, wrapping errors with fmt.Errorf, or checking errors with errors.Is/As. Use for "error handling", "fmt.Errorf", "errors.Is", "errors.As", "sentinel error", "custom error", or "%w". Do NOT use for general Go development, debugging runtime panics, or logging strategy.
2installs
Added on
NPX Install
npx skill4agent add notque/claude-code-toolkit go-error-handlingTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →Go Error Handling Skill
Operator Context
This skill operates as an operator for Go error handling implementation, configuring Claude's behavior for idiomatic, context-rich error propagation. It implements the Pattern Application architectural approach -- select the right error pattern (wrap, sentinel, custom type), apply it consistently, verify the error chain is preserved.
Hardcoded Behaviors (Always Apply)
- CLAUDE.md Compliance: Read and follow repository CLAUDE.md before implementing error handling
- Over-Engineering Prevention: Use the simplest error pattern that fits. Do not create custom types when a sentinel suffices, or sentinels when a simple wrap is enough
- Always Check Errors: Every function that returns an error must have its error checked or explicitly ignored with
_ = fn() - Always Wrap with Context: Never use naked . Every wrap adds meaningful context describing the operation that failed
return err - Preserve Error Chains: Use verb in
%wso callers can usefmt.Errorfanderrors.Iserrors.As - Error Messages Form a Narrative: When read top-to-bottom, wrapped errors tell the story of what happened
Default Behaviors (ON unless disabled)
- Lower-case Error Messages: Error strings start lower-case, no trailing punctuation (Go convention)
- Contextual Identifiers: Include relevant IDs, keys, or filenames in wrap messages
- Sentinel Errors for API Boundaries: Define sentinel errors for conditions callers need to check
- Custom Types for Rich Context: Use custom error types only when callers need structured data
- errors.Is for Values, errors.As for Types: Never use string matching or type assertions for error checks
- HTTP Status Mapping: Map domain errors to HTTP status codes at the handler boundary
Optional Behaviors (OFF unless enabled)
- gopls MCP Error Tracing: Use to find all usages of sentinel errors across the codebase,
go_symbol_referencesto verify error handling correctness after edits. Fallback:go_diagnosticsCLI or LSP toolgopls references - Error Wrapping Audit: Scan for naked statements and missing
return errverbs%w - Table-Driven Error Tests: Generate table-driven tests for error paths
Available Scripts
- — Detect bare
scripts/check-errors.shand log-and-return anti-patterns. Runreturn errfor options.bash scripts/check-errors.sh --help
What This Skill CAN Do
- Guide idiomatic error wrapping with and
fmt.Errorf%w - Define and use sentinel errors (package-level vars)
errors.New - Create custom error types that implement the interface
error - Implement and
errors.Ischecks throughout error chainserrors.As - Map domain errors to HTTP status codes at handler boundaries
- Verify error propagation in table-driven tests
What This Skill CANNOT Do
- Debug runtime panics or stack traces (use systematic-debugging instead)
- Implement structured logging (use logging-specific guidance instead)
- Handle Go concurrency error patterns (use go-concurrency instead)
- Design error monitoring or alerting systems (out of scope)
Instructions
Step 1: Identify the Error Pattern Needed
Before writing any error handling code, determine which pattern fits:
| Situation | Pattern | Example |
|---|---|---|
| Operation failed, caller does not check type | Wrap with context | |
| Caller needs to check for specific condition | Sentinel error | |
| Caller needs structured error data | Custom error type | |
| Error at HTTP boundary | Status mapping | |
Step 2: Wrap Errors with Context
Every error return should add context describing the operation that failed. Error messages form a readable narrative when chained.
go
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load config from %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config JSON from %s: %w", path, err)
}
return &cfg, nil
}
// Output: "parse config JSON from /etc/app.json: invalid character..."Rules for wrap messages:
- Describe the operation, not the error (not
"load config")"error loading config" - Include identifying data (filename, ID, key)
- Use to preserve the error chain
%w - Start lower-case, no trailing punctuation
Step 3: Define Sentinel Errors When Callers Need to Check
Sentinel errors are package-level variables for conditions callers must handle specifically.
go
package mypackage
import "errors"
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
)
func GetUser(ctx context.Context, id string) (*User, error) {
user, err := db.QueryUser(ctx, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("query user %s: %w", id, err)
}
return user, nil
}
// Caller:
user, err := GetUser(ctx, id)
if errors.Is(err, ErrNotFound) {
// handle not found
}Step 4: Create Custom Error Types for Rich Context
Use custom types when callers need to extract structured data from errors.
go
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on field %q: %s", e.Field, e.Message)
}
func ValidateUser(u *User) error {
if u.Email == "" {
return &ValidationError{Field: "email", Message: "required"}
}
return nil
}
// Caller uses errors.As:
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Field %s failed: %s\n", valErr.Field, valErr.Message)
}Step 5: Use errors.Is and errors.As Correctly
errors.Is checks if any error in the chain matches a specific value:
go
if errors.Is(err, ErrNotFound) { /* handle */ }
if errors.Is(err, context.Canceled) { /* cancelled */ }
if errors.Is(err, os.ErrNotExist) { /* file missing */ }errors.As extracts a specific error type from the chain:
go
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("Path: %s, Op: %s\n", pathErr.Path, pathErr.Op)
}
var netErr net.Error
if errors.As(err, &netErr) {
if netErr.Timeout() { /* handle timeout */ }
}Step 6: Map Errors to HTTP Status at Boundaries
Error-to-status mapping belongs at the HTTP handler level, not in domain logic.
go
func errorToStatus(err error) int {
switch {
case errors.Is(err, ErrNotFound):
return http.StatusNotFound
case errors.Is(err, ErrUnauthorized):
return http.StatusUnauthorized
case errors.Is(err, ErrInvalidInput):
return http.StatusBadRequest
default:
return http.StatusInternalServerError
}
}Step 7: Test Error Paths
Use table-driven tests to verify error handling:
go
func TestProcessUser(t *testing.T) {
tests := []struct {
name string
input *User
wantErr error
}{
{
name: "nil user",
input: nil,
wantErr: ErrInvalidInput,
},
{
name: "missing email",
input: &User{Name: "test"},
wantErr: ErrInvalidInput,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ProcessUser(tt.input)
if tt.wantErr != nil {
if !errors.Is(err, tt.wantErr) {
t.Errorf("got error %v, want %v", err, tt.wantErr)
}
} else if err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
}Step 8: Verify Error Handling Completeness
Before completing, check:
- All errors checked -- no unchecked returns
- Context added -- each wrap describes the operation
- verb used -- error chain preserved
%w - Narrative formed -- error messages readable top-to-bottom
- Sentinel errors defined -- for conditions callers must check
- /
errors.Isused -- no string comparison or type assertionerrors.As - HTTP status mapped -- at handler boundaries only
Error Handling
Error: "error chain broken -- errors.Is returns false"
Cause: Used instead of in , or created a new error instead of wrapping
Solution:
%v%wfmt.Errorf- Check all calls use
fmt.Errorffor the error argument%w - Ensure sentinel errors are not re-created (use the same )
var - Verify custom types implement if they wrap inner errors
Unwrap()
Error: "redundant error context in logs"
Cause: Same context added at multiple call levels, or wrapping errors that already contain the info
Solution:
- Each level should add only its own context, not repeat caller context
- One wrap per call boundary -- do not double-wrap at the same level
Error: "sentinel error comparison fails across packages"
Cause: Error was recreated with instead of using the exported variable
Solution:
errors.New- Import and reference the package-level directly
var ErrX - Never shadow sentinel errors with local calls
errors.New
Anti-Patterns
Anti-Pattern 1: Wrapping Without Meaningful Context
What it looks like: or
Why wrong: "error" and "failed" add zero information. The chain becomes noise.
Do instead: Describe the operation:
return fmt.Errorf("error: %w", err)return fmt.Errorf("failed: %w", err)return fmt.Errorf("load user %s from database: %w", userID, err)Anti-Pattern 2: Naked Error Returns
What it looks like: without wrapping
Why wrong: Loses context at every call boundary. Final error message lacks the narrative chain.
Do instead: Always wrap:
return errreturn fmt.Errorf("outer operation: %w", err)Anti-Pattern 3: Silently Ignoring Errors
What it looks like: without checking the return, or without comment
Why wrong: Silent failures cause data corruption, resource leaks, or hard-to-debug issues downstream.
Do instead: Check and log: or explicitly ignore:
file.Close()data, _ := fetchData()if err := file.Close(); err != nil { log.Printf("close file: %v", err) }_ = file.Close()Anti-Pattern 4: String Matching for Error Checks
What it looks like:
Why wrong: Fragile. Error messages change. Breaks across wrapped errors.
Do instead: Use or
if strings.Contains(err.Error(), "not found")errors.Is(err, ErrNotFound)errors.As(err, &target)Anti-Pattern 5: Over-Wrapping at the Same Level
What it looks like:
Why wrong: Redundant wrapping creates unreadable error chains.
Do instead: One clear wrap per call level:
return fmt.Errorf("process: error processing: %w", fmt.Errorf("processing failed: %w", err))return fmt.Errorf("process user request: %w", err)References
This skill uses these shared patterns:
- Anti-Rationalization - Prevents shortcut rationalizations
- Verification Checklist - Pre-completion checks
Domain-Specific Anti-Rationalization
| Rationalization | Why It's Wrong | Required Action |
|---|---|---|
| "Simple error, no need to wrap" | Unwrapped errors lose context at every level | Always wrap with |
| "String check is fine for now" | String matching breaks when messages change | Use |
| "No one will check this error" | You cannot predict caller needs | Define sentinel if it crosses a package boundary |
| "Custom type is overkill" | Evaluate the actual need, but do not skip if callers need structured data | Match pattern to situation (Step 1 table) |
Reference Files
- : Extended patterns -- gopls tracing, HTTP handler patterns, error wrapping in middleware
${CLAUDE_SKILL_DIR}/references/patterns.md