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:
Handle and address the error immediately Return the error to the caller 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 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
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:
Return the error (wrapped or verbatim) for the caller to handle Log and degrade gracefully (don't return the error) 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 Pattern Guidance Return type Always use error interface, not concrete types Error strings Lowercase, no punctuation Ignoring errors Comment explaining why it's safe In-band errors Avoid; use multiple returns Error flow Handle errors first, no else clauses Error type choice Match needed + dynamic → custom type; static → sentinel Sentinel errors Use errors.Is for checking %v vs %w %v for boundaries, %w for chain preservation %w placement Always at the end: "context: %w" Handle once Choose ONE: return, log+degrade, or match+handle Logging Don'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 references/ERROR-TYPES.md - Sentinel errors, structured error types, choosing error types, and checking errors references/WRAPPING.md - Error wrapping with %v vs %w, placement, adding context, and logging