>_
GolangStepByStep
Software Engineer

Error Strategy

Wrapping, sentinel errors, typed errors, boundary handling

# What is Go's Error Philosophy?

In languages like Java or Python, an error is a bomb (an Exception). When it explodes, it shatters the current code path, flies up into the sky invisibly, and hopes someone in a try/catch block catches it before the entire program burns down.

In Go, an error is treated entirely differently. An error is just a normal package delivered to your door.

When a Go function finishes, it gives you two things: The item you requested, and an "error report". It is your immediate responsibility as the developer to look at the error report right then and there. If it says there was a failure, you must decide what to do immediately.

# Level 1: The Absolute Basics (Beginner)

An error in Go is not magic. It's simply an interface with one requirement: it must be able to return a string explaining what went wrong.

type error interface {
    Error() string
}

You create new, basic errors using errors.New().

package main

import (
    "errors"
    "fmt"
)

func Divide(a, b int) (int, error) {
    if b == 0 {
        // 1. Create a basic error
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

func main() {
    result, err := Divide(10, 0)
    
    // 2. The Golden Rule of Go: Always check the error immediately!
    if err != nil {
        fmt.Println("Uh oh:", err)
        return
    }
    
    fmt.Println("Result:", result)
}

# Level 2: Adding Breadcrumbs (Intermediate)

In real applications, an error might travel through five different functions before it is logged to a server. If the bottom function returns "file not found", your log is useless because you don't know what file or who asked for it.

To fix this, you must "Wrap" the error. Wrapping means putting the error inside a box, and writing a note on the box. You do this using fmt.Errorf and the magical %w verb.

func FetchConfig() error {
    _, err := os.ReadFile("app.json")
    if err != nil {
        // Wrapping the error!
        // Adds "failed to load config" but keeps the original error alive inside.
        return fmt.Errorf("failed to load config: %w", err)
    }
    return nil
}

// In main():
// err string -> "failed to load config: open app.json: no such file or directory" 

Crucial Rule: Never use %v to wrap an error. %v just turns the error into a dumb string, destroying the original error entirely. Always use %w.

# Level 3: Detecting Errors (Advanced)

Sometimes you don't just want to print the error. You want your program to make a decision based on exactly what failed.

If an error has been wrapped 5 times, how do you know if the original failure at the very bottom was a "Row Not Found" error? You use errors.Is().

Using errors.Is (For specific exact errors)

A "Sentinel Error" is a predefined error variable you test against. It compares values.

var ErrUserNotFound = errors.New("user not found in db")

func GetUser() error {
    return fmt.Errorf("database fetch: %w", ErrUserNotFound) // Wrapped!
}

func main() {
    err := GetUser()
    
    // Instead of doing err == ErrUserNotFound (which fails because it's wrapped)
    // We use errors.Is to drill down and check the core!
    if errors.Is(err, ErrUserNotFound) {
        fmt.Println("Action: Ask the user to register an account.")
    }
}

Using errors.As (For extracting detailed struct data)

Sometimes an error is a complex Struct containing data like HTTP Status codes or validation fields. You want to extract that data.

type RequestError struct {
    StatusCode int
    Err        error
}
// MUST implement the error interface
func (r *RequestError) Error() string { return fmt.Sprintf("status %d: %v", r.StatusCode, r.Err) }

func main() {
    err := CallAPI() // Let's pretend this fails
    
    // Create an empty variable of the type you want
    var reqErr *RequestError
    
    // errors.As hunts for this specific Type inside the wrap chain. 
    // If it finds it, it fills the 'reqErr' variable!
    if errors.As(err, &reqErr) {
        if reqErr.StatusCode == 404 {
            fmt.Println("Page not found! Redirect to home.")
        }
    }
}

# Level 4: Panics vs Errors (Expert)

Beginners often ask "How do I throw an exception in Go?" and discover the panic() function.

Do not use panic for regular errors.

A panic is meant for catastrophic programming mistakes where the application cannot safely continue.

  • Use Error: The user entered a bad password. The database server timed out. The file wasn't found on the hard drive.
  • Use Panic: You wrote a regex hardcoded in your app and formatted it wrong. You divided by a hardcoded zero. You tried to access array index 10 when the array is size 2. These are developer bugs.

⚡ Production Error Checklist

  • Never ignore an error with _ unless you truly, violently don't care (e.g., closing a network connection that is already failing).
  • When returning an error to an upper layer, wrap it using fmt.Errorf("doing action: %w", err) to preserve the stack breadcrumbs.
  • Use errors.Is() instead of == to match error values.
  • Use errors.As() when you need to inspect custom struct fields within the error.
practice & review