>_ Golang Step By Step
Software Engineer

Error Handling

Handle errors the Go way — explicit, composable, and clear

# The error Interface

In Go, errors are values. The built-in error interface has just one method:

type error interface {
    Error() string
}

// Creating errors
err1 := errors.New("something failed")
err2 := fmt.Errorf("failed to load %s", filename)

# The if err != nil Pattern

Go's signature error-handling pattern. Return errors as the last return value and check immediately:

func readConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("reading config: %w", err)
    }
    return data, nil
}

func main() {
    data, err := readConfig("app.json")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Loaded", len(data), "bytes")
}

# Custom Error Types

Create custom types that implement the error interface to carry additional context:

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{
            Field:   "age",
            Message: "must be non-negative",
        }
    }
    return nil
}

# Wrapping & Inspecting Errors

Use %w to wrap errors and errors.Is() / errors.As() to inspect the chain:

import ("errors"; "io"; "os")

var ErrNotFound = errors.New("not found")

func findUser(id int) error {
    return fmt.Errorf("findUser(%d): %w", id, ErrNotFound)
}

// Check the chain
err := findUser(42)
if errors.Is(err, ErrNotFound) {
    fmt.Println("user not found")
}

// Check for specific type
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println("path:", pathErr.Path)
}

# panic & recover

panic is for unrecoverable errors. recover catches panics in deferred functions:

func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic: %v", r)
        }
    }
    return a / b, nil
}

result, err := safeDivide(10, 0)
fmt.Println(result, err) // 0 panic: runtime error: ...

⚡ Key Takeaways

  • Errors are values, not exceptions — return and check them explicitly
  • Wrap errors with %w to preserve context along the call chain
  • Use errors.Is() for sentinel checks and errors.As() for type checks
  • panic is for programmer bugs, not expected errors
  • Custom error types carry structured context beyond a message string
practice & review