Error Handling Basics (Idiomatic Go)

Error handling in Go defines observability, retry safety, and incident clarity. This lesson covers wrapping strategy, sentinel vs typed errors, failure classification, and production-safe logging boundaries.

On this page

Error Handling Is an Operational Concern

In Go, errors are values. This simplicity is powerful, but in production systems, careless error handling leads to noisy logs, hidden root causes, and unreliable retry behavior.

Error design affects:

  • Incident debugging speed
  • Retry correctness
  • Alert accuracy
  • User-facing error clarity
  • System resilience

Real Production Failure: The Logged-But-Lost Root Cause

A service returned generic “internal error” messages while logging the real error deep inside a repository layer. The error was logged multiple times at different layers. During an outage, engineers saw thousands of duplicate logs without context of which request triggered which failure.

The actual root cause (database connection pool exhaustion) was hidden inside nested error strings.

Lesson: error wrapping and logging boundaries must be intentional.

Core Principle: Return, Wrap, Log Once

Errors should flow upward with context added at meaningful boundaries.

Basic Wrapping

if err != nil {
    return fmt.Errorf("fetch user %d: %w", userID, err)
}

The %w verb preserves the original error for unwrapping.

Sentinel Errors vs Typed Errors

Sentinel Error Example

var ErrNotFound = errors.New("not found")

Use sentinel errors sparingly. They are useful for common, stable conditions.

Typed Error Example

type ValidationError struct {
    Field string
    Msg   string
}

func (e ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}

Typed errors are preferable when you need structured inspection.

Using errors.Is and errors.As

errors.Is

if errors.Is(err, ErrNotFound) {
    // handle not found
}

errors.As

var vErr ValidationError
if errors.As(err, &vErr) {
    // handle validation error
}

Never compare error strings.

Failure Classification in Production

Not all errors are equal. Classify failures:

  • Permanent (validation errors)
  • Transient (network timeout)
  • Resource exhaustion (connection pool full)
  • Dependency outage
  • Programming bugs (nil pointer)

This classification drives retry behavior and alerting.

Retry Safety Foundations

Only retry transient errors.

if errors.Is(err, context.DeadlineExceeded) {
    // retry
}

Never blindly retry validation or logical errors.

Context Cancellation Awareness

Always propagate context errors properly.

if errors.Is(err, context.Canceled) {
    return err
}

Swallowing context cancellation causes stuck goroutines.

Logging Boundaries

Rule: log at the boundary, not everywhere.

  • Repository layer: return wrapped error
  • Service layer: add context, return
  • Transport layer (HTTP handler): log once with request metadata

Correct Logging Example

if err != nil {
    logger.Error("request failed",
        "path", r.URL.Path,
        "err", err,
    )
    http.Error(w, "internal error", 500)
}

Avoid logging inside deep layers unless absolutely necessary.

Anti-Patterns

  • Comparing error strings
  • Wrapping without %w
  • Logging the same error at multiple layers
  • Returning generic errors without context
  • Retrying every failure blindly

Error Taxonomy Strategy

Define clear error categories in larger systems:

  • Domain errors
  • Infrastructure errors
  • Transport errors

This enables cleaner metrics and alert grouping.

Debugging Deeply Wrapped Errors

fmt.Printf("%+v
", err)

Or inspect with errors.Unwrap manually.

Testing Error Paths

Production reliability requires testing failure paths explicitly.

func TestService_NotFound(t *testing.T) {
    _, err := svc.GetUser(999)
    if !errors.Is(err, ErrNotFound) {
        t.Fatalf("expected ErrNotFound")
    }
}

Operational Checklist

  • All wrapped errors use %w
  • errors.Is / errors.As used correctly
  • Errors classified for retry logic
  • Context errors propagated
  • Logged once at boundary
  • No string comparison for errors

Final Perspective

Error handling in Go is simple syntactically but complex architecturally. Your error strategy determines how quickly incidents are understood and resolved. Design errors as part of your observability system — not as afterthoughts.