Error Handling
The Error Interface
Section titled “The Error Interface”Go’s error handling is convention-based. The error interface has a single method:
type error interface { Error() string}Any type implementing Error() string is an error. There is no exception mechanism — errors are Values, returned like any other value.
Idiomatic Error Pattern
Section titled “Idiomatic Error Pattern”Functions that can fail return an error as their last return value:
func Open(name string) (*File, error)func Read(r io.Reader) ([]byte, error)func Parse(s string) (T, error)The caller checks the error immediately:
f, err := os.Open("file.txt")if err != nil { return fmt.Errorf("failed to open file: %w", err)}defer f.Close()Sentinel Errors
Section titled “Sentinel Errors”Sentinel errors are predefined error values that the caller can compare against:
var ErrNotFound = errors.New("not found")
func Lookup(key string) (Value, error) { // ... if notFound { return Value{}, ErrNotFound } return val, nil}
v, err := Lookup("key")if errors.Is(err, ErrNotFound) { // handle not found}Use errors.Is (Go 1.13+) instead of == for comparison, because errors.Is unwraps wrapped Errors:
if errors.Is(err, ErrNotFound) { ... }Custom Error Types
Section titled “Custom Error Types”Define custom error types when the caller needs to distinguish error kinds:
type ParseError struct { Line int Column int Msg string}
func (e *ParseError) Error() string { return fmt.Sprintf("parse error at %d:%d: %s", e.Line, e.Column, e.Msg)}Use errors.As to extract a specific error type from a wrapped error chain:
var perr *ParseErrorif errors.As(err, &perr) { fmt.Printf("parse error at line %d\n", perr.Line)}Error Wrapping
Section titled “Error Wrapping”Go 1.13 introduced fmt.Errorf with %w verb for error wrapping, along with errors.Is and errors.As for inspecting wrapped errors.
Wrapping with %w
Section titled “Wrapping with %w”func readConfig(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read config: %w", err) } // parse data...}fmt.Errorf("context: %w", err) creates a new error that wraps err. The wrapped error is Accessible via errors.Is and errors.As.
%w vs %v
Section titled “%w vs %v”%wwraps the error, making it accessible toerrors.Isanderrors.As.%v(or%s) formats the error as a string. The original error is not accessible.
err1 := fmt.Errorf("context: %w", originalErr) // wrappederr2 := fmt.Errorf("context: %v", originalErr) // not wrapped, just formatted
errors.Is(err1, originalErr) // trueerrors.Is(err2, originalErr) // falseerrors.Is
Section titled “errors.Is”Traverses the error chain to check if any error in the chain matches a target:
var ErrNotFound = errors.New("not found")
func process() error { return fmt.Errorf("processing: %w", ErrNotFound)}
err := process()fmt.Println(errors.Is(err, ErrNotFound)) // trueerrors.As
Section titled “errors.As”Traverses the error chain to find an error of a specific type:
type TimeoutError struct { Duration time.Duration}
func (e *TimeoutError) Error() string { return fmt.Sprintf("timed out after %v", e.Duration)}
err := someFunction()var terr *TimeoutErrorif errors.As(err, &terr) { fmt.Printf("timed out after %v\n", terr.Duration)}Custom Unwrap
Section titled “Custom Unwrap”To make a custom error type compatible with errors.Is and errors.AsImplement the Unwrap() error method:
type AppError struct { Code int Message string Err error}
func (e *AppError) Error() string { if e.Err != nil { return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err) } return fmt.Sprintf("[%d] %s", e.Code, e.Message)}
func (e *AppError) Unwrap() error { return e.Err}Multi-error
Section titled “Multi-error”Go 1.20 added errors.Join for combining multiple errors into one:
var errs []errorerrs = append(errs, err1)errs = append(errs, err2)
combined := errors.Join(errs...)fmt.Println(errors.Is(combined, err1)) // truefmt.Println(errors.Is(combined, err2)) // truePanic and Recover
Section titled “Panic and Recover”panic is for unrecoverable programming errors — bugs, not expected failures:
panic("invariant violated: x must be positive")panic(fmt.Sprintf("unexpected state: %v", state))Panics unwind the stack, running deferred functions. If unhandled, the program crashes.
Recover
Section titled “Recover”recover stops a panic and returns the panic value. It is only useful inside a deferred function:
func safeDivide(a, b float64) (result float64, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic recovered: %v", r) } }()
if b == 0 { panic("division by zero") } return a / b, nil}recover returns nil if there was no panic.
When to Panic
Section titled “When to Panic”Panics are appropriate for:
- Invariant violations (programming bugs)
- Unreachable code paths (
defaultin a type switch covering all cases) - Initialization failures in
init()functions
Panics are not appropriate for:
- File not found, network timeout, invalid user input — these are expected failures
- Any error that a caller might reasonably want to handle
Recover at the Top Level
Section titled “Recover at the Top Level”In servers, recover panics at the handler level to prevent a single panic from crashing the entire Process:
func handler(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { log.Printf("panic in handler: %v\n%s", err, debug.Stack()) http.Error(w, "internal server error", http.StatusInternalServerError) } }() // handler logic}Intuition
Section titled “Intuition”Errors are return values, not escape routes: Think of Go error handling like a delivery driver who always hands you a receipt — success or failure, you get a slip of paper. There’s no “exception” trap door that skips the rest of the function. You must look at the receipt before moving on. Error wrapping is like adding a sticky note to that receipt: “this failed because of that upstream failure,” building a chain of custody for debugging.
Why it matters: The (value, error) return pattern makes failure handling explicit and composable. You can’t accidentally forget an error path the way you can with try/catch, because the compiler warns you about unused variables and the code reads linearly.
The key insight: Go treats errors as ordinary values — returned, checked, and wrapped — making the happy path and the error path equally visible in the code.
Common Pitfalls
Section titled “Common Pitfalls”Ignoring errors.
result, _ := someFunc()silently discards errors. At minimum, log the error. In tests, uset.Fatal(err).Using
panicfor expected errors. Panics are for bugs. If a file might not exist, return an error. If a network request might time out, return an error.Comparing errors with
==. Useerrors.Isinstead. Direct comparison fails for wrapped errors and for custom error types that are not sentinel values.Creating error types without
Unwrap. If your error type wraps another error, implementUnwrap() errorso thaterrors.Isanderrors.Ascan traverse the chain.Using
%vinstead of%wwhen wrapping.%vformats the error as a string, losing the ability to inspect the chain witherrors.Is/errors.As.Recover outside of defer.
recoveronly works inside deferred functions. Calling it anywhere else always returnsnil.String-based error matching. Using
strings.Contains(err.Error(), "not found")is fragile. Use sentinel errors or custom error types witherrors.Is/errors.As.
Summary
Section titled “Summary”This topic covers the core concepts of error handling, including underlying theory, practical implementation, and key applications.
Key concepts include:
- core concepts and terminology
- algorithms and computational thinking
- practical implementation
- security and ethical considerations
- applications in the real world
Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Cross-References
Section titled “Cross-References”- Interfaces: The error interface and custom error type design patterns.
- Testing: Error assertion and inspection patterns in test suites.
- net/http: HTTP error handling and middleware error propagation.