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.
The Error Interface
Section titled “The Error Interface”Understanding Errors as Values
Section titled “Understanding Errors as Values”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).
Example
Section titled “Example”Custom Error Types
Section titled “Custom Error Types”When and Why
Section titled “When and Why”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 on a pointer receiver, and you have a custom error type. Callers use errors.As() to extract the custom type and access its fields.
Two conventions worth following from day one:
- Pointer receiver on
Error(). The standard library does this everywhere (*os.PathError,*json.SyntaxError,*net.OpError). With a value receiver, bothTand*Tsatisfyerror, so callers can’t tell which one to search for anderrors.As(err, &target)silently fails when the target kind doesn’t match what was returned. errors.As, never a bare type assertion.err.(*ValidationError)only inspects the outermost error. The moment anyone wraps it withfmt.Errorf("...: %w", err)- which the very next section teaches you to do - the assertion returnsfalseand your error handling silently disappears.errors.Aswalks the whole chain.
Implementation
Section titled “Implementation”Create custom error types when you need to include additional context:
Sentinel Errors
Section titled “Sentinel Errors”Sentinel errors are predefined error values for specific conditions:
Error Wrapping (Go 1.13+)
Section titled “Error Wrapping (Go 1.13+)”Wrap errors to add context while preserving the original error:
errors.Is and errors.As
Section titled “errors.Is and errors.As”Use errors.Is to check for specific errors and errors.As to extract typed errors:
Multiple Errors: errors.Join (Go 1.20+)
Section titled “Multiple Errors: errors.Join (Go 1.20+)”Everything so far assumed one error at a time. Real code often has several: a form with three invalid fields, a batch job where four of a hundred items failed, a shutdown sequence where two of five closers returned an error. Before Go 1.20 people concatenated strings and lost all structure.
Go 1.20 added two things:
errors.Join(errs ...error) error- returns an error wrapping all non-nil arguments. It returnsnilif every argument is nil, which makes the “collect then join” pattern below clean: no length check needed.- Multiple
%wverbs in onefmt.Errorf-fmt.Errorf("a: %w, b: %w", err1, err2)is now legal.
Both produce an error implementing Unwrap() []error (note: a slice, distinct from the single-error Unwrap() error). errors.Is and errors.As traverse the whole tree, so a joined error matches on any of its branches.
Error Handling Patterns
Section titled “Error Handling Patterns”Handle Once
Section titled “Handle Once”Handle errors at one place, don’t log and return:
Fail Fast
Section titled “Fail Fast”Check errors immediately, don’t defer error handling:
Key Takeaways
Section titled “Key Takeaways”- Errors are values - the
errorinterface is justError() string - Custom errors - create types when you need additional context
- Sentinel errors - predefined errors for known conditions
- Wrap with %w - use
fmt.Errorf("context: %w", err)to add context - errors.Is/As - check error chains without unwrapping manually; never bare type assertions, which break as soon as the error is wrapped
- errors.Join - report independent failures together (Go 1.20+);
%wmay appear more than once in oneErrorf - Pointer receiver on Error() -
*MyErroris the error type, matching the standard library - Handle once - either return an error or handle it, not both
Exercise
Section titled “Exercise”Error Chain Parser
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.
Practice
Section titled “Practice”Put the whole chain - custom types, sentinels, wrapping, errors.Is/errors.As - to work:
- Error Handling Exercises
- 1. Custom Errors - build typed errors and inspect them through a wrap
- 2. Retry Patterns - decide what is retryable from the error itself
Next up: Chapter 3: Pointers & Memory