Skip to content

Chapter 2: Error Handling Patterns

Go’s explicit error handling is a deliberate design choice. Rather than exceptions that can bubble up unexpectedly, Go makes error handling visible and mandatory.

Error handling in Go is one of its most controversial features. Coming from languages with try/catch, the explicit if err != nil checks feel verbose and repetitive. But this verbosity serves a purpose: it makes error handling visible, predictable, and impossible to ignore.

Go’s philosophy is that errors are expected outcomes, not exceptional conditions. A file might not exist. A network request might fail. A user might enter invalid input. These aren’t exceptions - they’re normal program flow. Go treats them as such, forcing you to explicitly decide how to handle each error.

This chapter covers error fundamentals, custom error types, error wrapping and unwrapping, sentinel errors, and practical patterns for real-world error handling. You’ll learn not just the mechanics, but the philosophy and best practices that make Go error handling effective.

In Go, errors are just values that implement the error interface. This interface is remarkably simple - just one method that returns a string:

type error interface {
Error() string
}

This simplicity is powerful. Any type can be an error by implementing Error(). Errors are returned like any other value. They can be stored, passed around, and inspected. There’s no special exception handling mechanism - just regular control flow.

Why this matters: Errors as values means you handle them where they occur, with full context. No catching exceptions three layers up the call stack and trying to figure out what went wrong. Error handling is local, explicit, and predictable.

The error return convention: Functions that can fail return (result, error). By convention, error is the last return value. nil error means success. Non-nil error means failure (and other return values should be ignored).

Create custom error types when you need to include additional context beyond a simple error message. Custom errors can carry structured data - field names, error codes, retry information, HTTP status codes - that callers can programmatically inspect and act on.

The standard errors.New("message") is fine for simple cases, but real applications need more. A custom ValidationError can specify which field failed validation. A custom HTTPError can include the status code. A custom RetryableError can signal that an operation should be retried.

When to use custom errors:

  • When callers need to make decisions based on error details
  • When errors need structured context (which field, what value, why it failed)
  • When different error types require different handling strategies
  • When you want type-safe error inspection without string matching

How they work: Define a struct, implement the Error() string method, and you have a custom error type. Callers can use type assertions (err.(*ValidationError)) or errors.As() to extract the custom type and access its fields.

Create custom error types when you need to include additional context:

Sentinel errors are predefined error values for specific conditions:

Wrap errors to add context while preserving the original error:

Use errors.Is to check for specific errors and errors.As to extract typed errors:

Handle errors at one place, don’t log and return:

Check errors immediately, don’t defer error handling:

  1. Errors are values - the error interface is just Error() string
  2. Custom errors - create types when you need additional context
  3. Sentinel errors - predefined errors for known conditions
  4. Wrap with %w - use fmt.Errorf("context: %w", err) to add context
  5. errors.Is/As - check error chains without unwrapping manually
  6. Handle once - either return an error or handle it, not both

Error Chain Parser

medium

Create a function that processes a file path. It should wrap errors at each level (validate path, check permissions, read file). Use errors.Is and errors.As to identify specific errors in the chain.


Chapter in progress
0 / 14 chapters completed

Next up: Chapter 3: Pointers & Memory