Get the FREE Ultimate OpenClaw Setup Guide →

go-error-handling

npx machina-cli add skill cxuu/golang-skills/go-error-handling --openclaw
Files (1)
SKILL.md
9.2 KB

Go Error Handling

In Go, errors are values - they are created by code and consumed by code. This skill covers how to return, structure, wrap, and handle errors effectively.


Returning Errors

Normative: Required per Google's canonical Go style guide.

Use the error Type

Use error to signal that a function can fail. By convention, error is the last result parameter.

// Good:
func Good() error { /* ... */ }

func GoodLookup() (*Result, error) {
    // ...
    if err != nil {
        return nil, err
    }
    return res, nil
}

Never return concrete error types from exported functions - a concrete nil pointer can become a non-nil interface value:

// Bad: Concrete error type can cause subtle bugs
func Bad() *os.PathError { /*...*/ }

// Good: Always return the error interface
func Good() error { /*...*/ }

Return Values on Error

When a function returns an error, callers must treat all non-error return values as unspecified unless explicitly documented. Commonly, non-error return values are their zero values.

Tip: Functions taking a context.Context should usually return an error so callers can determine if the context was cancelled.


Error Strings

Normative: Required per Google's canonical Go style guide.

Error strings should not be capitalized and should not end with punctuation:

// Bad:
err := fmt.Errorf("Something bad happened.")

// Good:
err := fmt.Errorf("something bad happened")

Exception: Error strings may start with a capital letter if they begin with an exported name, proper noun, or acronym.

Rationale: Error strings usually appear within other context before being printed.

For displayed messages (logs, test failures, API responses), capitalization is appropriate:

// Good:
log.Infof("Operation aborted: %v", err)
log.Errorf("Operation aborted: %v", err)
t.Errorf("Op(%q) failed unexpectedly; err=%v", args, err)

Handling Errors

Normative: Required per Google's canonical Go style guide.

Code that encounters an error must make a deliberate choice about how to handle it. Do not discard errors using _ variables.

When a function returns an error, do one of:

  1. Handle and address the error immediately
  2. Return the error to the caller
  3. In exceptional situations: call log.Fatal or (if absolutely necessary) panic

Intentionally Ignoring Errors

In rare cases where ignoring an error is appropriate, add a comment explaining why:

// Good:
var b *bytes.Buffer
n, _ := b.Write(p) // never returns a non-nil error

Using errgroup for Related Operations

When orchestrating related operations where only the first error is useful, errgroup provides a convenient abstraction:

// Good: errgroup handles cancellation and first-error semantics
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

Normative: Required per Google's canonical Go style guide.

Do not return special values like -1, nil, or empty string to signal errors:

// Bad: In-band error value
// Lookup returns the value for key or -1 if there is no mapping for key.
func Lookup(key string) int

// Bad: Caller mistakes can attribute errors to wrong function
return Parse(Lookup(missingKey))

Use multiple return values instead:

// Good: Explicit error or ok value
func Lookup(key string) (value string, ok bool)

// Good: Forces caller to handle the error case
value, ok := Lookup(key)
if !ok {
    return fmt.Errorf("no value for %q", key)
}
return Parse(value)

This prevents callers from writing Parse(Lookup(key)) - it causes a compile-time error since Lookup(key) has 2 outputs.


Indent Error Flow

Normative: Required per Google's canonical Go style guide.

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
}

Error Types

Advisory: Recommended best practice.

If callers need to distinguish different error conditions programmatically, give errors structure rather than relying on string matching. Choose the right error type based on whether callers need to match errors and whether messages are static or dynamic.

Quick decision table:

Caller needs to match?Message typeUse
Nostaticerrors.New("message")
Nodynamicfmt.Errorf("msg: %v", val)
Yesstaticvar ErrFoo = errors.New("...")
Yesdynamiccustom error type

For detailed coverage of sentinel errors, structured error types, and error checking patterns, see references/ERROR-TYPES.md.


Error Wrapping

Advisory: Recommended best practice.

The choice between %v and %w significantly impacts how errors are propagated and inspected:

  • Use %v: At system boundaries, for logging, to hide internal details
  • Use %w: To preserve error chain for errors.Is/errors.As inspection

Key rules:

  • Place %w at the end: "context message: %w"
  • Add context that callers don't have; don't duplicate existing info
  • If annotation adds nothing, just return err directly

For detailed coverage of wrapping patterns, placement, adding context, and logging best practices, see references/WRAPPING.md.


Handle Errors Once

Source: Uber Go Style Guide

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

Never log AND return - this causes duplicate logging as callers up the stack will 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)
    }
}

Quick Reference

PatternGuidance
Return typeAlways use error interface, not concrete types
Error stringsLowercase, no punctuation
Ignoring errorsComment explaining why it's safe
In-band errorsAvoid; use multiple returns
Error flowHandle errors first, no else clauses
Error type choiceMatch needed + dynamic → custom type; static → sentinel
Sentinel errorsUse errors.Is for checking
%v vs %w%v for boundaries, %w for chain preservation
%w placementAlways at the end: "context: %w"
Handle onceChoose ONE: return, log+degrade, or match+handle
LoggingDon't log and return; let caller decide

See Also

  • go-style-core: Core Go style principles and formatting
  • go-naming: Naming conventions including error naming (ErrFoo)
  • go-testing: Testing patterns including error testing
  • go-defensive: Defensive programming including panic handling
  • go-linting: Linting tools that catch error handling issues

Reference Files

Source

git clone https://github.com/cxuu/golang-skills/blob/main/skills/go-error-handling/SKILL.mdView on GitHub

Overview

Go errors are values. This skill teaches how to return, wrap, and handle errors in Go using Google and Uber style guides. You’ll learn when to wrap with %w, how to use sentinel errors, how to choose error types, structure error flow, and log effectively.

How This Skill Works

In Go, functions return an error as the last value and callers must handle it. The skill covers returning the error interface, avoiding exported functions from returning concrete error types, using %w to wrap errors for context, and applying standard practices for error strings and logging. It also highlights coordinating related operations with errgroup and avoiding in-band error signaling.

When to Use It

  • Writing Go functions that can fail and must return an error
  • Propagating errors up the call stack with contextual wrapping
  • Using sentinel errors (e.g., for not-found or permission-denied conditions)
  • Coordinating concurrent tasks with errgroup to surface the first error
  • Avoiding in-band error signaling and ensuring explicit error returns

Quick Start

  1. Step 1: Define functions to return (T, error) when failure is possible
  2. Step 2: Check if err != nil; return the appropriate zero value and err (or wrap and return)
  3. Step 3: Wrap errors with %w to add context and preserve the error chain; use errgroup for related tasks

Best Practices

  • Return the error type, not a concrete error value or type from exported functions
  • Wrap errors with context using %w to preserve the error chain
  • Error strings should not be capitalized or end with punctuation (exceptions exist for starting with an exported name)
  • Do not discard errors; handle them or return them to the caller; avoid using _ unless you annotate why
  • Use errgroup for related operations to simplify cancellation and first-error semantics

Example Use Cases

  • Wrap a database query error with context using %w to preserve the original error
  • Return sentinel errors like os.ErrNotExist to indicate specific conditions
  • Avoid exporting concrete *os.PathError from public functions; return error instead
  • Use errgroup.WithContext to run parallel tasks and surface the first error
  • Check context cancellation and return its error to callers to propagate cancellation

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers ↗