
Go Error Handling
- 929 installs
- 137 repo stars
- Updated June 20, 2026
- cxuu/golang-skills
go-error-handling is a Go coding skill that enforces clean, readable error handling patterns for developers who want to avoid nested branches and duplicated logging in Go services.
About
go-error-handling is a cxuu/golang-skills guide to error flow in Go, centered on the handle-once principle and logging decisions that keep the normal code path visible. It teaches indent-error-flow by handling errors before proceeding, avoiding else-clause normal paths, and cautioning against if-with-initializer patterns for long-lived variables. Developers reach for go-error-handling when refactoring noisy if err != nil blocks or standardizing logging across a Go codebase. The skill is pattern-oriented with concrete good-and-bad Go snippets rather than a standalone linter binary.
- Indent error flow so normal path stays unindented and easy to follow
- Avoid if-with-initializer for variables used across many lines
- Handle each error only once: return, log-and-degrade, or match-and-handle
- Never both log and return the same error
- 3 core error-flow decision rules
Go Error Handling by the numbers
- 929 all-time installs (skills.sh)
- +39 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #426 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cxuu/golang-skills --skill go-error-handlingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 929 |
|---|---|
| repo stars | ★ 137 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 20, 2026 |
| Repository | cxuu/golang-skills ↗ |
How do you structure idiomatic Go error handling?
Enforce clean, readable Go error handling patterns that prevent nested code and duplicated logging.
Who is it for?
Go backend developers refactoring error-heavy functions who want readable control flow aligned with handle-once logging discipline.
Skip if: Teams needing automated lint enforcement across naming or packages instead of manual error-flow pattern guidance.
When should I use this skill?
The user writes or reviews Go error handling, mentions handle-once logging, nested if err blocks, or wants cleaner error flow in Go packages.
What you get
Go code with early error returns, single-point logging, and an unindented normal execution path.
- Refactored Go error-handling patterns
- Readable unindented normal code paths
Files
Go Error Handling
Compatibility:errors.Is,errors.As, and%wwrapping require Go 1.13+; structured logging examples may uselog/slogfrom Go 1.21+.
Resource Routing
scripts/check-errors.sh- Run when checking string-based error matching, bare error propagation, and log-and-return patterns.scripts/check-errors-ast.go- Implementation helper invoked bycheck-errors.sh; patch this when changing error-flow analysis behavior.references/ERROR-FLOW.md- Read when deciding where to handle, wrap, log, or return errors.references/ERROR-TYPES.md- Read when choosing sentinel errors, typed errors, or opaque errors.references/WRAPPING.md- Read when choosing%wversus%vor crossing package boundaries.
In Go, errors are values — they are created by code and consumed by code.
Choosing an Error Strategy
1. System boundary (RPC, IPC, storage)? → Wrap with %v to avoid leaking internals 2. Caller needs to match specific conditions? → Sentinel or typed error, wrap with %w 3. Caller just needs debugging context? → fmt.Errorf("...: %w", err) 4. Leaf function, no wrapping needed? → Return the error directly
Default: wrap with %w and place it at the end of the format string.
---
Core Rules
Never Return Concrete Error Types
Never return concrete error types from exported functions — a concrete nil pointer can become a non-nil interface:
// Bad: Concrete type can cause subtle bugs
func Bad() *os.PathError { /*...*/ }
// Good: Always return the error interface
func Good() error { /*...*/ }Error Strings
Error strings should not be capitalized and should not end with punctuation. Exception: exported names, proper nouns, or acronyms.
// Bad
err := fmt.Errorf("Something bad happened.")
// Good
err := fmt.Errorf("something bad happened")For displayed messages (logs, test failures, API responses), capitalization is appropriate.
Return Values on Error
When a function returns an error, callers must treat all non-error return values as unspecified unless explicitly documented.
Tip: Functions taking a context.Context should usually return an error so callers can determine if the context was cancelled.
---
Handling Errors
When encountering an error, make a deliberate choice — do not discard with _:
1. Handle immediately — address the error and continue 2. Return to caller — optionally wrapped with context 3. In exceptional cases — log.Fatal or panic
To intentionally ignore: add a comment explaining why.
n, _ := b.Write(p) // never returns a non-nil errorFor related concurrent operations, use `errgroup`:
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return task1(ctx) })
g.Go(func() error { return task2(ctx) })
if err := g.Wait(); err != nil { return err }Avoid In-Band Errors
Don't return -1, nil, or empty string to signal errors. Use multiple returns:
// Bad: In-band error value
func Lookup(key string) int // returns -1 for missing
// Good: Explicit error or ok value
func Lookup(key string) (string, bool)This prevents callers from writing Parse(Lookup(key)) — it causes a compile-time error since Lookup(key) has 2 outputs.
---
Error Flow
Handle errors before normal code. Early returns keep the happy path unindented:
// Good: Error first, normal code unindented
if err != nil {
return err
}
// normal codeHandle errors once — either log or return, never both:
Error encountered?
├─ Caller can act on it? → Return (with context via %w)
├─ Top of call chain? → Log and handle
└─ Neither? → Log at appropriate level, continue---
Error Types
Advisory: Recommended best practice.
| Caller needs to match? | Message type | Use |
|---|---|---|
| No | static | errors.New("message") |
| No | dynamic | fmt.Errorf("msg: %v", val) |
| Yes | static | var ErrFoo = errors.New("...") |
| Yes | dynamic | custom error type |
Default: Wrap with fmt.Errorf("...: %w", err). Escalate to sentinels for errors.Is(), to custom types for errors.As().
---
Error Wrapping
Advisory: Recommended best practice.
- Use `%v`: At system boundaries, for logging, to hide internal details
- Use `%w`: To preserve error chain for
errors.Is/errors.As
Key rules: Place %w at the end. Add context callers don't have. If annotation adds nothing, return err directly.
Validation: After implementing error handling, runbash scripts/check-errors.shto detect common anti-patterns. Then rungo vet ./...to catch additional issues.
---
Related Skills
- Error naming: See go-naming when naming sentinel errors (
ErrFoo) or custom error types - Testing errors: See go-testing when testing error semantics with
errors.Is/errors.Asor writing error-checking helpers - Panic handling: See go-defensive when deciding between panic and error returns, or writing recover guards
- Guard clauses: See go-control-flow when structuring early-return error flow or reducing nesting
- Logging decisions: See go-logging when choosing log levels, configuring structured logging, or deciding what context to include in log messages
Error Flow Patterns
Detailed patterns for error flow, the handle-once principle, and logging decisions.
Indent Error Flow
Handle errors before proceeding with normal code. This improves readability by enabling the reader to find the normal path quickly.
// Good: Error handling first, normal code unindented
if err != nil {
// error handling
return // or continue, etc.
}
// normal code// Bad: Normal code hidden in else clause
if err != nil {
// error handling
} else {
// normal code that looks abnormal due to indentation
}Avoid If-with-Initializer for Long-Lived Variables
If you use a variable for more than a few lines, move the declaration out:
// Good: Declaration separate from error check
x, err := f()
if err != nil {
return err
}
// lots of code that uses x
// across multiple lines// Bad: Variable scoped to else block, hard to read
if x, err := f(); err != nil {
return err
} else {
// lots of code that uses x
// across multiple lines
}---
Handle Errors Once
When a caller receives an error, it should handle each error only once. Choose ONE response:
1. Return the error (wrapped or verbatim) for the caller to handle 2. Log and degrade gracefully (don't return the error) 3. Match and handle specific error cases, return others
If you return an error, don't log it yourself — let the caller handle it. Logging and returning the same error is the most common "handle errors once" violation, causing duplicate noise as callers up the stack also handle the error.
// Bad: Logs AND returns - causes noise in logs
u, err := getUser(id)
if err != nil {
log.Printf("Could not get user %q: %v", id, err)
return err // Callers will also log this!
}
// Good: Wrap and return - let caller decide how to handle
u, err := getUser(id)
if err != nil {
return fmt.Errorf("get user %q: %w", id, err)
}
// Good: Log and degrade gracefully (don't return error)
if err := emitMetrics(); err != nil {
// Failure to write metrics should not break the application
log.Printf("Could not emit metrics: %v", err)
}
// Continue execution...
// Good: Match specific errors, return others
tz, err := getUserTimeZone(id)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
// User doesn't exist. Use UTC.
tz = time.UTC
} else {
return fmt.Errorf("get user %q: %w", id, err)
}
}---
Logging vs Returning Errors
Handle an error exactly once — either log it or return it, never both.
Decision Flow
Error encountered?
├─ Can the caller act on it? → Return the error (with context via %w)
├─ Is this the top of the call chain? → Log and handle (return HTTP status, exit, etc.)
└─ Neither? → Log at appropriate level and continueDon't Log and Return
// Bad: error is logged AND returned — appears twice in logs
func process(ctx context.Context, id string) error {
result, err := fetch(ctx, id)
if err != nil {
log.Printf("failed to fetch %s: %v", id, err)
return fmt.Errorf("fetching %s: %w", id, err)
}
return handle(result)
}
// Good: return with context — let the caller decide whether to log
func process(ctx context.Context, id string) error {
result, err := fetch(ctx, id)
if err != nil {
return fmt.Errorf("fetching %s: %w", id, err)
}
return handle(result)
}Structured Logging
Prefer structured logging (slog in Go 1.21+, or log/slog-compatible libraries) over log.Printf for production code:
// Good: structured fields are machine-parseable
slog.Error("fetch failed", "id", id, "err", err)
// Avoid: unstructured string interpolation
log.Printf("fetch failed for %s: %v", id, err)Verbosity Levels
| Level | Use for |
|---|---|
| Error | Actionable failures that need attention |
| Warn | Degraded behavior that doesn't require immediate action |
| Info | Key lifecycle events (startup, shutdown, config loaded) |
| Debug | Diagnostic detail useful during development |
Error Types Reference
This reference covers structured error types, sentinel errors, and how to choose the right error type for your use case.
---
Error Structure
The error-type decision table is in the parent skill (SKILL.md § Error Types).
This reference covers: expanded code examples, sentinel errors, error checking
witherrors.Is/errors.As, and structured error types.
Key considerations:
- Does the caller need to match the error with
errors.Isorerrors.As? - Is the error message static or does it require runtime values?
- Exported error variables/types become part of your public API
// No matching needed, static message
func Open() error {
return errors.New("could not open")
}
// Matching needed, static message - export a sentinel
var ErrCouldNotOpen = errors.New("could not open")
func Open() error {
return ErrCouldNotOpen
}
// Matching needed, dynamic message - use custom type
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}
func Open(file string) error {
return &NotFoundError{File: file}
}---
Sentinel Errors
The simplest structured errors are unparameterized global values:
// Good: Sentinel errors for programmatic checking
var (
// ErrDuplicate occurs if this animal has already been seen.
ErrDuplicate = errors.New("duplicate")
// ErrMarsupial occurs because we're allergic to marsupials.
ErrMarsupial = errors.New("marsupials are not supported")
)
func process(animal Animal) error {
switch {
case seen[animal]:
return ErrDuplicate
case marsupial(animal):
return ErrMarsupial
}
seen[animal] = true
return nil
}---
Checking Errors
For direct comparison (when errors are not wrapped):
// Good: Direct comparison with sentinel
switch err := process(an); err {
case ErrDuplicate:
return fmt.Errorf("feed %q: %v", an, err)
case ErrMarsupial:
alternate := an.BackupAnimal()
return handlePet(alternate)
}When errors may be wrapped, use errors.Is:
// Good: Works with wrapped errors
switch err := process(an); {
case errors.Is(err, ErrDuplicate):
return fmt.Errorf("feed %q: %v", an, err)
case errors.Is(err, ErrMarsupial):
// Try to recover...
}Never match errors based on string content:
// Bad: Fragile string matching
if regexp.MatchString(`duplicate`, err.Error()) {...}
if regexp.MatchString(`marsupial`, err.Error()) {...}---
Structured Error Types
For errors needing additional programmatic information, use struct types:
// Good: Structured error with accessible fields
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
func (e *PathError) Unwrap() error { return e.Err }Callers can use errors.As to extract the structured error:
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("Failed path:", pathErr.Path)
}---
Quick Reference
| Scenario | Error Type |
|---|---|
| No matching needed, static message | errors.New("message") |
| No matching needed, dynamic message | fmt.Errorf("msg: %v", val) |
| Matching needed, static message | var ErrFoo = errors.New(...) |
| Matching needed, dynamic message | custom struct type |
| Checking sentinel errors | errors.Is(err, ErrFoo) |
| Extracting structured errors | errors.As(err, &target) |
Error Wrapping Reference
This reference covers error wrapping with %v vs %w, placement conventions, adding context to errors, and logging best practices.
---
Wrapping Errors: %v vs %w
Advisory: Recommended best practice.
The choice between %v and %w significantly impacts how errors are propagated and inspected.
Use %v for Simple Annotation
Use %v when you want to:
- Add context without preserving the error chain for programmatic inspection
- Create fresh, independent errors (especially at system boundaries like
RPC/IPC)
- Log or display errors to humans
// Good: %v at system boundary - hide internal details
func (s *Server) SuggestFortune(ctx context.Context, req *pb.Request) (*pb.Response, error) {
if err != nil {
return nil, fmt.Errorf("couldn't find fortune database: %v", err)
}
}Use %w for Error Chain Preservation
Use %w when you want callers to programmatically inspect the underlying error:
// Good: %w preserves error chain for errors.Is/errors.As
func (s *Server) internalFunction(ctx context.Context) error {
if err != nil {
return fmt.Errorf("couldn't find remote file: %w", err)
}
}
// Caller can now check:
if errors.Is(err, fs.ErrNotExist) {
// Handle not found case
}When to Use Each
Use %w when:
- Adding context while preserving the original error for programmatic inspection
- You explicitly document and test the underlying errors you expose
Use %v when:
- At system boundaries (RPC, IPC, storage) to translate to canonical error space
- Logging or displaying to humans
- Creating independent errors that hide implementation details
---
Placement of %w
Advisory: Recommended best practice.
Place %w at the end of the error string so error text mirrors error chain structure:
// Good: %w at end - prints newest to oldest
err1 := fmt.Errorf("err1")
err2 := fmt.Errorf("err2: %w", err1)
err3 := fmt.Errorf("err3: %w", err2)
fmt.Println(err3) // err3: err2: err1// Bad: %w at start - prints oldest to newest (confusing)
err1 := fmt.Errorf("err1")
err2 := fmt.Errorf("%w: err2", err1)
err3 := fmt.Errorf("%w: err3", err2)
fmt.Println(err3) // err1: err2: err3// Bad: %w in middle - incoherent order
err1 := fmt.Errorf("err1")
err2 := fmt.Errorf("err2-1 %w err2-2", err1)
err3 := fmt.Errorf("err3-1 %w err3-2", err2)
fmt.Println(err3) // err3-1 err2-1 err1 err2-2 err3-2Pattern: Use the form context message: %w
---
Adding Information to Errors
Advisory: Recommended best practice.
Add Context, Not Redundancy
Add information that you have but the caller/callee might not. Avoid duplicating information the underlying error already provides:
// Good: Adds meaningful context
if err := os.Open("settings.txt"); err != nil {
return fmt.Errorf("launch codes unavailable: %v", err)
}
// Output: launch codes unavailable: open settings.txt: no such file or directory// Bad: Duplicates the filename
if err := os.Open("settings.txt"); err != nil {
return fmt.Errorf("could not open settings.txt: %v", err)
}
// Output: could not open settings.txt: open settings.txt: no such file or directoryDon't Annotate Without Purpose
If the annotation only indicates failure without adding information, just return the error:
// Bad: Annotation adds nothing
return fmt.Errorf("failed: %v", err)
// Good: Just return the error
return err---
Logging Errors
Advisory: Recommended best practice.
When you do log errors, use log/slog (Go 1.21+) with structured key-value pairs and the appropriate level:
- `slog.Error`: Reserve for actionable issues that need investigation.
- `slog.Warn`: For issues that may need attention but aren't immediately
actionable.
- `slog.Debug`: For development tracing — only emitted when the handler's
level is set to LevelDebug.
// Good: Structured logging with appropriate levels
for _, q := range queries {
slog.Debug("handling query", "query", q)
q.Run()
}
// Good: Guard expensive formatting behind a level check
if slog.Default().Enabled(context.Background(), slog.LevelDebug) {
slog.Debug("query plan", "explain", q.Explain())
}
// Bad: Expensive call evaluated even when debug logging is disabled
slog.Debug("query plan", "explain", q.Explain())Protect Sensitive Information
Be careful with PII (Personally Identifiable Information) in log messages. Many log sinks are not appropriate for sensitive user data.
---
Quick Reference
| Pattern | Guidance |
|---|---|
%v | Use at system boundaries, for logging, to hide details |
%w | Use to preserve error chain for programmatic inspection |
%w placement | Always at the end: "context: %w" |
| Adding context | Add new info, don't duplicate existing info |
| Empty annotation | Just return err instead of fmt.Errorf("failed: %v", err) |
| Logging | Don't log and return; use appropriate log levels |
package main
import (
"encoding/json"
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
)
const version = "1.1.0"
type finding struct {
File string `json:"file"`
Line int `json:"line"`
Rule string `json:"rule"`
Message string `json:"message"`
}
type options struct {
jsonOutput bool
checkBareReturn bool
limit int
target string
help bool
version bool
}
func usage() {
fmt.Fprintf(os.Stdout, `check-errors.sh v%s - Check Go code for common error handling anti-patterns
USAGE
bash check-errors.sh [options] [path]
OPTIONS
-h, --help Show this help message
-v, --version Show version
--json Output results as JSON
--no-bare-return Skip the bare 'return err' check (high false-positive rate)
--limit N Show at most N results (default: all)
`, version)
}
func main() {
opts, err := parseArgs(os.Args[1:])
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(2)
}
if opts.help {
usage()
return
}
if opts.version {
fmt.Printf("check-errors.sh v%s\n", version)
return
}
files, err := findGoFiles(opts.target)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(2)
}
if len(files) == 0 {
if opts.jsonOutput {
fmt.Println(`{"findings":[],"total":0,"truncated":false,"status":"no_go_files"}`)
} else {
fmt.Printf("No Go files found in: %s\n", opts.target)
}
return
}
findings := []finding{}
for _, file := range files {
fileFindings, err := analyzeFile(file, opts.checkBareReturn)
if err != nil {
fmt.Fprintf(os.Stderr, "error: parse %s: %v\n", file, err)
os.Exit(2)
}
findings = append(findings, fileFindings...)
}
sort.SliceStable(findings, func(i, j int) bool {
if findings[i].File == findings[j].File {
if findings[i].Line == findings[j].Line {
return findings[i].Rule < findings[j].Rule
}
return findings[i].Line < findings[j].Line
}
return findings[i].File < findings[j].File
})
total := len(findings)
truncated := false
if opts.limit > 0 && total > opts.limit {
findings = findings[:opts.limit]
truncated = true
}
if opts.jsonOutput {
out := struct {
Findings []finding `json:"findings"`
Total int `json:"total"`
Truncated bool `json:"truncated"`
}{Findings: findings, Total: total, Truncated: truncated}
data, err := json.Marshal(out)
if err != nil {
fmt.Fprintf(os.Stderr, "error: marshal JSON: %v\n", err)
os.Exit(2)
}
fmt.Println(string(data))
} else {
if total == 0 {
fmt.Println("No error handling anti-patterns found.")
return
}
fmt.Println("Error handling anti-patterns found:")
fmt.Println()
for _, item := range findings {
fmt.Printf(" %s:%d [%s] %s\n", item.File, item.Line, item.Rule, item.Message)
}
if truncated {
fmt.Printf(" ... and %d more (use --limit to adjust)\n", total-opts.limit)
}
fmt.Println()
fmt.Printf("Total: %d finding(s)\n", total)
}
if total > 0 {
os.Exit(1)
}
}
func analyzeFile(path string, checkBareReturn bool) ([]finding, error) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, path, nil, 0)
if err != nil {
return nil, err
}
var findings []finding
var logLines []int
var errReturnLines []int
ast.Inspect(file, func(n ast.Node) bool {
switch node := n.(type) {
case *ast.BinaryExpr:
if node.Op != token.EQL && node.Op != token.NEQ {
return true
}
line := fset.Position(node.Pos()).Line
switch {
case isErrorCall(node.X) && isStringLiteral(node.Y):
findings = append(findings, finding{
File: path,
Line: line,
Rule: "string-error-compare",
Message: "comparing err.Error() to string; use errors.Is() or errors.As() instead",
})
case isStringLiteral(node.X) && isErrorCall(node.Y):
findings = append(findings, finding{
File: path,
Line: line,
Rule: "string-error-compare",
Message: "comparing string to err.Error(); use errors.Is() or errors.As() instead",
})
}
case *ast.CallExpr:
line := fset.Position(node.Pos()).Line
if isStringsContainsErrorCall(node) {
findings = append(findings, finding{
File: path,
Line: line,
Rule: "string-error-compare",
Message: "using strings.Contains on err.Error(); use errors.Is() or errors.As() instead",
})
}
if isLogCallWithErr(node) {
logLines = append(logLines, line)
}
case *ast.ReturnStmt:
if returnsErr(node) {
line := fset.Position(node.Return).Line
errReturnLines = append(errReturnLines, line)
if checkBareReturn {
findings = append(findings, finding{
File: path,
Line: line,
Rule: "bare-return-err",
Message: "returning err without wrapping context; consider fmt.Errorf('...: %w', err)",
})
}
}
}
return true
})
for _, retLine := range errReturnLines {
for i := len(logLines) - 1; i >= 0; i-- {
logLine := logLines[i]
if logLine >= retLine {
continue
}
if retLine-logLine > 5 {
break
}
findings = append(findings, finding{
File: path,
Line: logLine,
Rule: "log-and-return",
Message: fmt.Sprintf("error is both logged (line %d) and returned (line %d); handle errors once", logLine, retLine),
})
break
}
}
return findings, nil
}
func isErrorCall(expr ast.Expr) bool {
call, ok := expr.(*ast.CallExpr)
if !ok || len(call.Args) != 0 {
return false
}
sel, ok := call.Fun.(*ast.SelectorExpr)
return ok && sel.Sel.Name == "Error"
}
func isStringLiteral(expr ast.Expr) bool {
lit, ok := expr.(*ast.BasicLit)
return ok && lit.Kind == token.STRING
}
func isStringsContainsErrorCall(call *ast.CallExpr) bool {
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok || sel.Sel.Name != "Contains" {
return false
}
pkg, ok := sel.X.(*ast.Ident)
if !ok || pkg.Name != "strings" || len(call.Args) == 0 {
return false
}
return containsErrorCall(call.Args[0])
}
func containsErrorCall(expr ast.Expr) bool {
found := false
ast.Inspect(expr, func(n ast.Node) bool {
if found {
return false
}
if e, ok := n.(ast.Expr); ok && isErrorCall(e) {
found = true
return false
}
return true
})
return found
}
func isLogCallWithErr(call *ast.CallExpr) bool {
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return false
}
x, ok := sel.X.(*ast.Ident)
if !ok {
return false
}
switch x.Name {
case "log", "logger", "slog":
default:
return false
}
for _, arg := range call.Args {
if containsIdent(arg, "err") {
return true
}
}
return false
}
func containsIdent(expr ast.Expr, name string) bool {
found := false
ast.Inspect(expr, func(n ast.Node) bool {
if found {
return false
}
ident, ok := n.(*ast.Ident)
if ok && ident.Name == name {
found = true
return false
}
return true
})
return found
}
func returnsErr(stmt *ast.ReturnStmt) bool {
if len(stmt.Results) == 0 {
return false
}
ident, ok := stmt.Results[len(stmt.Results)-1].(*ast.Ident)
return ok && ident.Name == "err"
}
func findGoFiles(target string) ([]string, error) {
info, err := os.Stat(target)
if err == nil {
if !info.IsDir() {
if strings.HasSuffix(target, ".go") && !strings.HasSuffix(target, "_test.go") {
return []string{target}, nil
}
return nil, nil
}
return walkGoFiles(target)
}
if strings.HasSuffix(target, "/...") {
dir := strings.TrimSuffix(target, "/...")
if dir == "" {
dir = "."
}
if info, statErr := os.Stat(dir); statErr == nil && info.IsDir() {
return walkGoFiles(dir)
}
}
return nil, fmt.Errorf("path not found: %s", target)
}
func walkGoFiles(root string) ([]string, error) {
var files []string
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
switch d.Name() {
case ".git", "vendor":
return filepath.SkipDir
}
return nil
}
if strings.HasSuffix(path, ".go") && !strings.HasSuffix(path, "_test.go") {
files = append(files, path)
}
return nil
})
sort.Strings(files)
return files, err
}
func parseArgs(args []string) (options, error) {
opts := options{target: ".", checkBareReturn: true}
var positionals []string
for i := 0; i < len(args); i++ {
arg := args[i]
switch {
case arg == "-h" || arg == "--help":
opts.help = true
case arg == "-v" || arg == "--version":
opts.version = true
case arg == "--json":
opts.jsonOutput = true
case arg == "--no-bare-return":
opts.checkBareReturn = false
case arg == "--limit":
if i+1 >= len(args) {
return opts, fmt.Errorf("--limit requires a number")
}
i++
limit, err := parseNonNegativeInt(args[i])
if err != nil {
return opts, fmt.Errorf("--limit must be a non-negative integer, got: %s", args[i])
}
opts.limit = limit
case strings.HasPrefix(arg, "--limit="):
value := strings.TrimPrefix(arg, "--limit=")
limit, err := parseNonNegativeInt(value)
if err != nil {
return opts, fmt.Errorf("--limit must be a non-negative integer, got: %s", value)
}
opts.limit = limit
case strings.HasPrefix(arg, "-"):
return opts, fmt.Errorf("unknown option: %s", arg)
default:
positionals = append(positionals, arg)
}
}
if len(positionals) > 1 {
return opts, fmt.Errorf("expected at most one path")
}
if len(positionals) == 1 {
opts.target = positionals[0]
}
return opts, nil
}
func parseNonNegativeInt(s string) (int, error) {
if s == "" {
return 0, fmt.Errorf("empty")
}
n := 0
for _, r := range s {
if r < '0' || r > '9' {
return 0, fmt.Errorf("invalid")
}
n = n*10 + int(r-'0')
}
return n, nil
}
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VERSION="1.1.0"
for arg in "$@"; do
case "$arg" in
-h|--help)
cat <<EOF
check-errors.sh v$VERSION - Check Go code for common error handling anti-patterns
USAGE
bash check-errors.sh [options] [path]
OPTIONS
-h, --help Show this help message
-v, --version Show version
--json Output results as JSON
--no-bare-return Skip the bare 'return err' check (high false-positive rate)
--limit N Show at most N results (default: all)
EOF
exit 0
;;
-v|--version)
echo "check-errors.sh v$VERSION"
exit 0
;;
esac
done
if ! command -v go >/dev/null 2>&1; then
echo "error: go is not installed or not in PATH" >&2
exit 2
fi
CACHE_ROOT="${XDG_CACHE_HOME:-${HOME:-${TMPDIR:-/tmp}}/.cache}/golang-skills"
if ! mkdir -p "$CACHE_ROOT"; then
CACHE_ROOT="${TMPDIR:-/tmp}/golang-skills-cache"
mkdir -p "$CACHE_ROOT"
fi
SRC="$SCRIPT_DIR/check-errors-ast.go"
STAMP="$(cksum "$SRC" | awk '{print $1 "-" $2}')"
BIN="$CACHE_ROOT/check-errors-ast-$STAMP"
if [[ ! -x "$BIN" ]]; then
GOCACHE="${GOCACHE:-$CACHE_ROOT/go-build}" go build -o "$BIN" "$SRC"
fi
exec "$BIN" "$@"
Related skills
FAQ
What is the handle-once principle in go-error-handling?
go-error-handling applies handle-once so each error is logged or handled at a single point, preventing duplicated log statements while keeping the normal Go execution path unindented and easy to read.
How should normal code appear after errors in Go?
go-error-handling recommends handling errors first and returning or continuing before normal code runs, avoiding else branches that hide the primary execution path behind extra indentation.
Is Go Error Handling safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.