Go Error Handler

v1.0.0

Analyze Go error handling patterns — detect swallowed errors, missing error wrapping, sentinel vs custom error usage, type assertions, and idiomatic error pr...

0· 19· 1 versions· 0 current· 0 all-time· Updated 6h ago· MIT-0

Install

openclaw skills install go-error-handler

Go Error Handler Analyzer

Deep analysis of error handling patterns in Go codebases. Detects swallowed errors, missing wrapping context, incorrect sentinel usage, unsafe type assertions, and non-idiomatic patterns. Produces prioritized findings aligned with Go proverbs and stdlib conventions.

Use when: reviewing Go code for production readiness, auditing error handling, or establishing team conventions.

Analysis Steps

1. Project Discovery

cat go.mod 2>/dev/null | head -10
find . -name "*.go" -not -path '*/vendor/*' | wc -l
find . -type d -name "errors" -o -name "errs" -not -path '*/vendor/*' 2>/dev/null
grep "pkg/errors\|go.uber.org/multierr\|cockroachdb/errors" go.mod 2>/dev/null

Determine: Go version (affects errors.Is/As availability), error library usage, custom error infrastructure.

2. Swallowed Errors

Most critical check. A swallowed error is an err return value that is ignored.

# Explicit underscore ignoring error
grep -rn '\b_\s*=.*(' --include="*.go" . 2>/dev/null | grep -v 'vendor/\|_test.go' | head -25

# err assigned but never checked
grep -rn 'err\s*=' --include="*.go" . 2>/dev/null | grep -v 'if.*err\|return.*err\|vendor/' | head -20

# defer with ignored error (defer f.Close())
grep -rn 'defer.*Close()\|defer.*Flush()\|defer.*Rollback()' --include="*.go" . 2>/dev/null | grep -v 'vendor/' | head -15

# http.Error without return (handler continues after error response)
grep -A2 'http\.Error(' --include="*.go" . 2>/dev/null | grep -v 'return\|vendor/' | head -15

Severity guide:

  • Critical: ignored errors from DB, file I/O, network calls
  • High: _ = someFunc() on functions that can fail meaningfully
  • Medium: defer f.Close() without error handling — use named return + defer closure
  • Low: ignoring fmt.Fprintf to stdout (acceptable in CLIs)

3. Error Wrapping Analysis

# Proper wrapping with %w (Go 1.13+)
grep -rn 'fmt\.Errorf.*%w' --include="*.go" . 2>/dev/null | grep -v 'vendor/' | head -15

# %v/%s instead of %w (loses error chain)
grep -rn 'fmt\.Errorf.*%[vs].*err\b' --include="*.go" . 2>/dev/null | grep -v 'vendor/' | head -15

# Bare return of error without wrapping (loses context)
grep -rn 'return.*err$' --include="*.go" . 2>/dev/null | grep -v 'nil\|vendor/\|_test.go' | head -20

# Error message conventions (should not start with capital or end with punctuation)
grep -rn 'fmt\.Errorf("[ ]*[A-Z]' --include="*.go" . 2>/dev/null | grep -v 'vendor/' | head -10
grep -rn 'errors\.New(".*\.")' --include="*.go" . 2>/dev/null | grep -v 'vendor/' | head -10

Flag:

  • %v instead of %w: fmt.Errorf("failed: %v", err) breaks errors.Is()/errors.As() — use %w
  • Bare error return: return err across package boundaries loses call-site context — wrap with fmt.Errorf("operation: %w", err)
  • Capitalized/punctuated error messages: Go convention is lowercase, no trailing period (errors compose)

4. Sentinel & Custom Error Patterns

# Sentinel errors
grep -rn 'var\s\+Err[A-Z].*=\s*errors\.New' --include="*.go" . 2>/dev/null | grep -v 'vendor/' | head -15

# String comparison instead of errors.Is (fragile)
grep -rn 'err\.Error()\s*==' --include="*.go" . 2>/dev/null | grep -v 'vendor/' | head -10

# Direct equality (breaks with wrapping)
grep -rn 'err\s*==\s*Err\|err\s*!=\s*Err' --include="*.go" . 2>/dev/null | grep -v 'vendor/' | head -10

# errors.Is / errors.As usage (correct patterns)
grep -rn 'errors\.Is(\|errors\.As(' --include="*.go" . 2>/dev/null | grep -v 'vendor/' | head -15

# Custom error types
grep -rn 'func.*Error()\s*string' --include="*.go" . 2>/dev/null | grep -v 'vendor/\|_test.go' | head -15

# Type assertion on errors (fragile with wrapping)
grep -rn 'err\.(\*' --include="*.go" . 2>/dev/null | grep -v 'vendor/' | head -10

Flag:

  • String comparison: err.Error() == "not found" — use errors.Is(err, ErrNotFound)
  • Direct equality: err == ErrNotFound fails if wrapped — use errors.Is()
  • Type assertion: err.(*MyError) fails on wrapped errors — use errors.As()
  • Missing Unwrap: custom error types wrapping other errors must implement Unwrap() error
  • Repeated string errors: errors.New("not found") in multiple places should be a sentinel

5. HTTP Handler & Goroutine Errors

# Internal errors leaked to client (security risk)
grep -rn 'http\.Error(w,.*err\.Error()' --include="*.go" . 2>/dev/null | grep -v 'vendor/' | head -10

# Panic in non-test code
grep -rn 'panic(' --include="*.go" . 2>/dev/null | grep -v 'vendor/\|_test.go' | head -10

# Goroutines without error propagation
grep -B2 -A10 'go func()' --include="*.go" . 2>/dev/null | grep -v 'vendor/' | head -30

# errgroup usage
grep -rn 'errgroup\|g\.Go\|group\.Go' --include="*.go" . 2>/dev/null | grep -v 'vendor/' | head -10

Flag:

  • Internal error leaked: http.Error(w, err.Error(), 500) sends raw errors to clients — log internally, return generic message
  • Missing return after http.Error: handler continues, potentially writing multiple responses
  • Goroutine with no error channel/errgroup: errors silently lost
  • Missing recover in long-running goroutines: panic crashes entire program
  • errgroup without context: use errgroup.WithContext to cancel remaining goroutines on first error

6. Error Test Coverage

grep -rn 'wantErr\|expectErr\|shouldErr' --include="*_test.go" . 2>/dev/null | grep -v 'vendor/' | head -15

# Packages with no error path tests
for pkg in $(find . -name "*.go" -not -name "*_test.go" -not -path '*/vendor/*' | sed 's|/[^/]*$||' | sort -u); do
  if [ -z "$(grep -rl 'wantErr\|shouldErr' ${pkg}/*_test.go 2>/dev/null)" ]; then
    echo "NO_ERROR_TESTS: $pkg"
  fi
done | head -10

Output Template

# Go Error Handling Analysis — [Module Name]

## Summary
- Files: N | Go: 1.XX | Error lib: stdlib/pkg/errors
- Critical: N | Warnings: N | Error test coverage: low/moderate/good

## Critical Findings
### [C1] Swallowed Database Error
- **File**: internal/repo/user.go:45
- **Code**: `rows, _ := db.Query(query, args...)`
- **Fix**: Handle error, return `fmt.Errorf("query users: %w", err)`

### [C2] Internal Error Leaked to Client
- **File**: internal/handler/order.go:78
- **Fix**: Log error, return `http.Error(w, "internal server error", 500)`

## Error Wrapping Issues
| File | Line | Issue | Fix |
|------|------|-------|-----|
| repo/user.go | 23 | `%v` not `%w` | Change to `%w` |
| service/order.go | 45 | Bare `return err` | Add context wrapping |

## Sentinel Error Inventory
| Package | Sentinel | Checked With |
|---------|----------|-------------|
| repo | ErrNotFound | errors.Is (correct) |
| auth | ErrExpired | err == (fix: errors.Is) |

## Recommendations
1. Replace N instances of `_ = operation()` with error handling
2. Change N `%v` to `%w` in fmt.Errorf calls
3. Add wrapping context to N bare `return err` statements
4. Install recovery middleware for HTTP server
5. Add error path tests to N packages

Error Handling Checklist

RuleSeverityGo Proverb
Never ignore errorsCritical"Errors are values"
Wrap with context at boundariesHighEach layer adds context
Use %w not %vHighPreserve error chains
Use errors.Is not ==HighWorks across wrapping
Use errors.As not type assertionHighWorks across wrapping
Return after http.ErrorCriticalPrevent double-write
Don't leak internals to clientsCriticalSecurity boundary
Propagate errors from goroutinesHighSilent failures are worst

Tips

  • Run errcheck ./... to catch unchecked errors mechanically
  • Use golangci-lint with wrapcheck, errcheck, goerr113 linters
  • For multi-error scenarios use errors.Join (Go 1.20+) or go.uber.org/multierr
  • Always handle defer Close() errors for writers — flushed data may fail to write

Version tags

latestvk97ab3gvn1fk0prh9tc50gffbx85wk75