Error Handling in Go

The 3 AM Alert That Taught Me Everything

It was 3:17 AM when PagerDuty woke me up. Our payment processing service was down. The logs showed:

panic: runtime error: invalid memory address or nil pointer dereference

The code that caused it:

func processPayment(orderID string) error {
    order := getOrder(orderID)  // Returned nil on not found
    // Crashed here - didn't check for nil!
    amount := order.Total
    charge(order.CustomerID, amount)
    return nil
}

I had ignored the error from getOrder. In Python, this would have raised an exception I could catch. In Go, I just got a panic and 2 hours of downtime.

The fix was embarrassingly simple:

func processPayment(orderID string) error {
    order, err := getOrder(orderID)
    if err != nil {
        return fmt.Errorf("failed to get order %s: %w", orderID, err)
    }
    
    if err := charge(order.CustomerID, order.Total); err != nil {
        return fmt.Errorf("failed to charge customer: %w", err)
    }
    
    return nil
}

That 3 AM incident taught me: explicit error handling isn't verbose - it's essential. This article covers everything I learned about Go's error philosophy.


The Error Interface

Go's error handling is built on a simple interface:

Any type with an Error() method satisfies this interface.

Creating Errors

Checking Errors

This pattern appears everywhere in Go code:


Error Handling Patterns

Early Return

Most idiomatic - exit early on errors:

Handle and Continue

Wrap and Return

Add context to errors:

The %w verb wraps the error, preserving the original.


Error Wrapping (Go 1.13+)

Why Wrap Errors?

Unwrapping Errors

Real example from my API:


Custom Error Types

Struct Errors

Real Example: HTTP Errors

From my REST API:


Sentinel Errors

Pre-defined errors for comparison:

Usage:

My convention for sentinel errors:


Panic and Recover

When to Panic

Rule: Almost never! Panics are for programming errors, not expected errors.

Acceptable panic use cases:

  1. Initialization errors (can't continue)

  2. Programmer errors (index out of bounds)

  3. Unrecoverable situations

Recover from Panic

Real example - HTTP server shouldn't crash on handler panic:


Practical Error Handling Example

My file processing pipeline:


Error Handling Best Practices

1. Don't Ignore Errors

2. Add Context When Wrapping

3. Handle Errors at the Right Level

4. Use Custom Error Types for Complex Cases

5. Log and Return


Errors vs Exceptions

Coming from languages with exceptions, Go's approach felt strange:

Exceptions (Python/Java):

  • Hidden control flow

  • Catch multiple layers up

  • Easy to forget

  • Stack unwinding

Go Errors:

  • Explicit in function signature

  • Must check at each level

  • Can't forget (compiler enforces)

  • Clear error flow

The 3 AM incident proved Go's approach right: I can't ignore errors by accident.


Your Challenge

Build a configuration loader with proper error handling:


Key Takeaways

  1. Errors are values - returned explicitly, not thrown

  2. Check every error - compiler won't let you forget

  3. Wrap errors with context - use %w verb

  4. Use errors.Is and errors.As - check error types safely

  5. Custom error types - for complex error handling

  6. Panic rarely - only for programming errors

  7. Recover in servers - don't crash on panics


What I Learned

The 3 AM payment incident transformed my view on error handling:

Before: Error checking feels verbose and repetitive After: Explicit errors prevent production disasters

In Python, I'd write:

Simple! But which exceptions can be raised? Where do they come from? I didn't know until production.

In Go:

More lines? Yes. But every error path is visible. Every failure is handled. No surprises at 3 AM.

Go's error handling isn't verbose - it's honest.


Next: Concurrency - Goroutines and Channels

In the next article, we'll explore Go's killer feature: built-in concurrency with goroutines and channels. You'll learn how the word go transformed my approach to parallel processing.

Last updated