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.Fprintfto 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)breakserrors.Is()/errors.As()— use%w - Bare error return:
return erracross package boundaries loses call-site context — wrap withfmt.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"— useerrors.Is(err, ErrNotFound) - Direct equality:
err == ErrNotFoundfails if wrapped — useerrors.Is() - Type assertion:
err.(*MyError)fails on wrapped errors — useerrors.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.WithContextto 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
| Rule | Severity | Go Proverb |
|---|---|---|
| Never ignore errors | Critical | "Errors are values" |
| Wrap with context at boundaries | High | Each layer adds context |
| Use %w not %v | High | Preserve error chains |
| Use errors.Is not == | High | Works across wrapping |
| Use errors.As not type assertion | High | Works across wrapping |
| Return after http.Error | Critical | Prevent double-write |
| Don't leak internals to clients | Critical | Security boundary |
| Propagate errors from goroutines | High | Silent failures are worst |
Tips
- Run
errcheck ./...to catch unchecked errors mechanically - Use
golangci-lintwithwrapcheck,errcheck,goerr113linters - For multi-error scenarios use
errors.Join(Go 1.20+) orgo.uber.org/multierr - Always handle
defer Close()errors for writers — flushed data may fail to write