Skip to content

Lesson 11: Error Handling

Big Picture

flowchart TD
    F["f() returns (result, error)"] --> C{"err != nil?"}
    C -->|yes| H["handle or wrap:<br/>fmt.Errorf(ctx: %w, err)"]
    C -->|no| U["use result"]
    H --> IS["errors.Is → compare sentinel"]
    H --> AS["errors.As → extract typed error"]
    P["panic"] -.->|"unwinds stack"| R["recover()<br/>inside deferred func"]
    note1["error = ordinary VALUE,<br/>not an exception"] --- F

Errors are values, not exceptions. if err != nil is verbose on purpose — it forces you to decide at each failure point. Reserve panic/recover for truly unrecoverable programmer errors.

Concepts

Basic Error Handling

result, err := someFunction()
if err != nil {
    // Handle error
    return err
}
// Use result

Creating Errors

// Simple error
err := errors.New("something went wrong")

// Formatted error
err := fmt.Errorf("failed to process %s", item)

Custom Error Types

type MyError struct {
    Code    int
    Message string
}

func (e *MyError) Error() string {
    return e.Message
}

Error Wrapping

// Wrap error with context
return fmt.Errorf("operation failed: %w", err)

// Check wrapped errors
if errors.Is(err, SomeError) { }
if errors.As(err, &myErr) { }

Defer and LIFO Order

A defer statement schedules a function call to run when the surrounding function is about to return. If a function schedules multiple deferred calls, Go executes them in LIFO (Last In, First Out) order: the last call deferred is the first one run. This stack-like behavior is useful when resources must be cleaned up in the reverse order in which they were acquired.

func deferredCalls() {
    defer fmt.Println("first")
    defer fmt.Println("second")
    defer fmt.Println("third")
}

// Output when deferredCalls returns:
// third
// second
// first

Panic and Recover

A deferred function can call recover to stop a panic while the stack is unwinding.

defer func() {
    if r := recover(); r != nil {
        // Handle panic
    }
}()
panic("critical error")

Code Walkthrough

package main

import (
    "errors"
    "fmt"
)

var ErrEmpty = errors.New("empty input")

func parse(s string) (int, error) {
    if s == "" {
        return 0, fmt.Errorf("parse: %w", ErrEmpty)
    }
    return len(s), nil
}

func main() {
    defer func() {
        if r := recover(); r != nil { fmt.Println("recovered:", r) }
    }()

    if _, err := parse(""); errors.Is(err, ErrEmpty) {
        fmt.Println("wrapped sentinel:", err)
    }

    panic("boom") // unwinds; deferred func recovers
}

Full program: code/11_errors.go.

Try it yourself

cd code
go run . errors

Exercises

  1. Define a sentinel var ErrNotFound = errors.New("not found"), wrap it, then detect it with errors.Is.
  2. Create a custom error type carrying a status code and extract it with errors.As.
  3. Write a function that panics, and recover from it in main via defer.

Check Your Understanding

#

What does the %w verb in fmt.Errorf do?

#

Where can recover() actually catch a panic?