Error Handling

Lesson 10: Error Handling

Real-World Analogy

Imagine you go to a restaurant to order food. The waiter tells you: "Sorry, this dish is sold out today." — This is an expected situation; you can order something else, and life goes on. This is error in Go.

But if the kitchen suddenly catches fire and everyone must evacuate immediately — This is an unexpected disaster. This is panic in Go.

Go's error handling philosophy is straightforward: handle anticipated problems gracefully, and only crash for unanticipated ones. Unlike other languages that mix all exceptions with try/catch, Go requires you to handle each error explicitly and individually.


Core Concepts

Concept Description
error interface Go's built-in error type with only one method: Error() string
Multiple return values Go functions typically return errors as the last return value
errors.New Creates a simple text error
fmt.Errorf Creates a formatted error (can wrap with %w)
errors.Is Checks if an error chain contains a specific error
errors.As Extracts a specific error type from an error chain
panic Triggers a runtime panic, crashing the program
recover Captures panic in defer to prevent program crash
Error wrapping Wrap underlying errors with %w or custom types to preserve context

Error Handling Flowchart

Function returns error
       │
       ▼
  err != nil ?
  ┌────┴────┐
  │ Yes     │ No
  ▼         ▼
Handle error  Continue
  │
  ├─ Recoverable → Log / Return default / Retry
  ├─ Needs reporting → Wrap and pass up
  └─ Unrecoverable → panic

Basic Syntax and Usage

1. error Interface

error is Go's built-in interface type, defined very concisely:

GO
// error interface definition (built-in, no import needed)
type error interface {
    Error() string
}

Any type that implements the Error() string method is an error.

2. Creating Errors

GO
package main

import (
    "errors"
    "fmt"
)

func main() {
    // Method 1: errors.New — create a simple text error
    err1 := errors.New("file not found")

    // Method 2: fmt.Errorf — create a formatted error
    filename := "config.yaml"
    err2 := fmt.Errorf("failed to read file %s", filename)

    // Method 3: fmt.Errorf + %w — wrap underlying error (recommended)
    baseErr := errors.New("permission denied")
    err3 := fmt.Errorf("cannot write log: %w", baseErr)

    fmt.Println(err1) // file not found
    fmt.Println(err2) // failed to read file config.yaml
    fmt.Println(err3) // cannot write log: permission denied
}

3. Checking Errors

GO
package main

import (
    "errors"
    "fmt"
)

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

func findUser(id int) (string, error) {
    if id <= 0 {
        return "", ErrNotFound
    }
    return "Alice", nil
}

func main() {
    name, err := findUser(0)
    if err != nil {
        // errors.Is checks if the error chain contains the target error
        if errors.Is(err, ErrNotFound) {
            fmt.Println("User not found, please check the ID")
        } else {
            fmt.Println("Unknown error:", err)
        }
        return
    }
    fmt.Println("Found user:", name)
}

4. Extracting Specific Error Types

GO
package main

import (
    "errors"
    "fmt"
)

// Custom error type
type ValidationError struct {
    Field   string
    Message string
}

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

func validateAge(age int) error {
    if age < 0 || age > 150 {
        return &ValidationError{
            Field:   "age",
            Message: "age must be between 0 and 150",
        }
    }
    return nil
}

func main() {
    err := validateAge(200)
    if err != nil {
        // errors.As extracts a specific type from the error chain
        var ve *ValidationError
        if errors.As(err, &ve) {
            fmt.Printf("Field: %s, Reason: %s\n", ve.Field, ve.Message)
        } else {
            fmt.Println("Other error:", err)
        }
    }
}

5. panic and recover

GO
package main

import "fmt"

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

    // Dividing by zero triggers a panic
    return a / b, nil
}

func main() {
    result, err := safeDivide(10, 0)
    if err != nil {
        fmt.Println("Error:", err) // Error: caught panic: runtime error: integer divide by zero
        return
    }
    fmt.Println("Result:", result)
}
💡 Tip: In Go, the vast majority of errors should be handled by returning error, not panic. Only use panic when the program truly cannot continue (e.g., initialization failure, unrecoverable logic errors).

💡 Tip: When wrapping errors with fmt.Errorf, always use %w instead of %v. %w preserves the original error, allowing errors.Is and errors.As to work correctly.

💡 Tip: Error messages should be lowercase sentences without ending punctuation, as they are often concatenated into larger error messages.


Examples

Example: Basic Error Handling (Difficulty ⭐)

Simulate a simple configuration file reader to demonstrate the most basic error return and checking pattern.

GO
package main

import (
    "errors"
    "fmt"
    "os"
)

// Define package-level sentinel errors
var (
    ErrFileNotFound  = errors.New("config file not found")
    ErrFileEmpty     = errors.New("config file is empty")
    ErrInvalidFormat = errors.New("invalid config file format")
)

// Config represents application configuration
type Config struct {
    Host string
    Port int
}

// LoadConfig loads configuration from a file (simulated)
func LoadConfig(path string) (*Config, error) {
    // Simulate file not found
    if path == "" {
        return nil, ErrFileNotFound
    }

    // Simulate reading file
    data, err := os.ReadFile(path)
    if err != nil {
        // Wrap underlying error, preserving original info
        return nil, fmt.Errorf("reading config file %s: %w", path, err)
    }

    // Check if file is empty
    if len(data) == 0 {
        return nil, ErrFileEmpty
    }

    // Simulate parsing config
    return &Config{
        Host: "localhost",
        Port: 8080,
    }, nil
}

func main() {
    // Try to load config
    config, err := LoadConfig("app.conf")
    if err != nil {
        // Use errors.Is to determine specific error type
        switch {
        case errors.Is(err, ErrFileNotFound):
            fmt.Println("Error: Please specify the config file path")
        case errors.Is(err, ErrFileEmpty):
            fmt.Println("Error: Config file is empty, please check contents")
        case errors.Is(err, ErrInvalidFormat):
            fmt.Println("Error: Config file format is invalid")
        default:
            fmt.Println("Error:", err)
        }
        return
    }

    fmt.Printf("Config loaded successfully: %s:%d\n", config.Host, config.Port)
}
▶ Try it Yourself

Output (when file doesn't exist):

Error: Please specify the config file path

Example: Error Wrapping and Chain Checking (Difficulty ⭐⭐)

Demonstrate error wrapping, propagation, and chain checking in multi-layer function calls.

GO
package main

import (
    "errors"
    "fmt"
)

// Define business errors
var (
    ErrUserNotFound = errors.New("user not found")
    ErrPermission   = errors.New("insufficient permissions")
)

// Represents database layer errors
type DatabaseError struct {
    Operation string
    Table     string
    Err       error
}

func (e *DatabaseError) Error() string {
    return fmt.Sprintf("database error [%s.%s]: %e", e.Table, e.Operation, e.Err)
}

func (e *DatabaseError) Unwrap() error {
    return e.Err
}

// UserRepository user repository
type UserRepository struct {
    users map[int]string
}

// FindByID finds a user by ID
func (r *UserRepository) FindByID(id int) (string, error) {
    name, exists := r.users[id]
    if !exists {
        // Wrap as database layer error
        return "", &DatabaseError{
            Operation: "SELECT",
            Table:     "users",
            Err:       ErrUserNotFound,
        }
    }
    return name, nil
}

// Service business service layer
type Service struct {
    repo *UserRepository
}

// GetUser gets user info
func (s *Service) GetUser(id int, requireAdmin bool) (string, error) {
    name, err := s.repo.FindByID(id)
    if err != nil {
        // Wrap upward, adding business context
        return "", fmt.Errorf("getting user info (id=%d): %w", id, err)
    }

    if requireAdmin && name != "admin" {
        return "", fmt.Errorf("user %s: %w", name, ErrPermission)
    }

    return name, nil
}

func main() {
    repo := &UserRepository{
        users: map[int]string{
            1: "admin",
            2: "Alice",
        },
    }
    svc := &Service{repo: repo}

    // Test scenarios
    testCases := []struct {
        name         string
        id           int
        requireAdmin bool
    }{
        {"Find admin", 1, false},
        {"Find regular user", 2, false},
        {"Find non-existent user", 99, false},
        {"Regular user accessing admin function", 2, true},
    }

    for _, tc := range testCases {
        fmt.Printf("--- %s ---\n", tc.name)
        user, err := svc.GetUser(tc.id, tc.requireAdmin)
        if err != nil {
            fmt.Printf("  Failed: %v\n", err)

            // errors.Is can traverse the error chain to find root cause
            if errors.Is(err, ErrUserNotFound) {
                fmt.Println("  Action: Prompt user to check ID")
            } else if errors.Is(err, ErrPermission) {
                fmt.Println("  Action: Prompt user to contact admin")
            }

            // errors.As can extract specific error types from intermediate layers
            var dbErr *DatabaseError
            if errors.As(err, &dbErr) {
                fmt.Printf("  DB details: table=%s, operation=%s\n", dbErr.Table, dbErr.Operation)
            }
        } else {
            fmt.Printf("  Success: User %s\n", user)
        }
        fmt.Println()
    }
}
▶ Try it Yourself

Output:

--- Find admin ---
  Success: User admin

--- Find regular user ---
  Success: User Alice

--- Find non-existent user ---
  Failed: getting user info (id=99): database error [users.SELECT]: user not found
  Action: Prompt user to check ID
  DB details: table=users, operation=SELECT

--- Regular user accessing admin function ---
  Failed: User Alice: insufficient permissions
  Action: Prompt user to contact admin

Example: Custom Error Types and recover in Practice (Difficulty ⭐⭐⭐)

Implement a complete HTTP request handler with custom error types, error codes, and a panic recovery middleware.

GO
package main

import (
    "errors"
    "fmt"
    "runtime/debug"
)

// ==================== Custom Error System ====================

// AppError application-level error with error code and context
type AppError struct {
    Code    int               // Business error code
    Message string            // User-friendly message
    Detail  string            // Developer debug info
    Cause   error             // Underlying cause
    Context map[string]string // Additional context
}

func (e *AppError) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Cause)
    }
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

func (e *AppError) Unwrap() error {
    return e.Cause
}

// Is implements custom error comparison logic
func (e *AppError) Is(target error) bool {
    t, ok := target.(*AppError)
    if !ok {
        return false
    }
    return e.Code == t.Code
}

// NewAppError creates a new application error
func NewAppError(code int, message string) *AppError {
    return &AppError{
        Code:    code,
        Message: message,
        Context: make(map[string]string),
    }
}

// WithCause sets the underlying cause
func (e *AppError) WithCause(err error) *AppError {
    e.Cause = err
    return e
}

// WithDetail sets debug details
func (e *AppError) WithDetail(detail string) *AppError {
    e.Detail = detail
    return e
}

// WithContext adds context information
func (e *AppError) WithContext(key, value string) *AppError {
    e.Context[key] = value
    return e
}

// Predefined error codes
var (
    ErrBadRequest   = NewAppError(400, "bad request")
    ErrUnauthorized = NewAppError(401, "unauthorized access")
    ErrForbidden    = NewAppError(403, "forbidden")
    ErrNotFound     = NewAppError(404, "resource not found")
    ErrInternal     = NewAppError(500, "internal server error")
)

// ==================== Simulated Business Layer ====================

// Request simulates an HTTP request
type Request struct {
    UserID  int
    Path    string
    IsAdmin bool
}

// Response simulates an HTTP response
type Response struct {
    StatusCode int
    Body       string
}

// validateRequest validates request parameters
func validateRequest(req *Request) error {
    if req.UserID <= 0 {
        return ErrBadRequest.
            WithDetail("user_id must be a positive integer").
            WithContext("user_id", fmt.Sprintf("%d", req.UserID))
    }
    if req.Path == "" {
        return ErrBadRequest.
            WithDetail("path cannot be empty")
    }
    return nil
}

// authenticate authentication
func authenticate(req *Request) error {
    if req.UserID == 0 {
        return ErrUnauthorized.WithDetail("missing authentication credentials")
    }
    return nil
}

// authorize authorization
func authorize(req *Request) error {
    if !req.IsAdmin {
        return ErrForbidden.
            WithDetail("admin privileges required").
            WithContext("user_id", fmt.Sprintf("%d", req.UserID))
    }
    return nil
}

// findResource finds a resource (may trigger panic)
func findResource(path string) (string, error) {
    // Simulate a bug that causes panic
    if path == "/crash" {
        var p *int
        *p = 1 // Nil pointer dereference, triggers panic
    }

    resources := map[string]string{
        "/users":   "User list",
        "/profile": "User profile",
    }

    data, exists := resources[path]
    if !exists {
        return "", ErrNotFound.
            WithDetail(fmt.Sprintf("resource for path %s not found", path))
    }
    return data, nil
}

// handleRequest processes a request (business logic)
func handleRequest(req *Request) (*Response, error) {
    // 1. Validate parameters
    if err := validateRequest(req); err != nil {
        return nil, err
    }

    // 2. Authenticate
    if err := authenticate(req); err != nil {
        return nil, err
    }

    // 3. Authorize (only for admin paths)
    if req.Path == "/admin" {
        if err := authorize(req); err != nil {
            return nil, err
        }
    }

    // 4. Find resource
    data, err := findResource(req.Path)
    if err != nil {
        return nil, err
    }

    return &Response{
        StatusCode: 200,
        Body:       data,
    }, nil
}

// ==================== Recovery Middleware ====================

// RecoveryMiddleware panic recovery middleware
func RecoveryMiddleware(handler func(*Request) (*Response, error)) func(*Request) (*Response, error) {
    return func(req *Request) (resp *Response, err error) {
        defer func() {
            if r := recover(); r != nil {
                // Log panic stack trace
                stack := string(debug.Stack())
                fmt.Printf("[PANIC] %v\nStack:\n%s\n", r, stack)

                // Return internal error instead of crashing
                err = ErrInternal.
                    WithDetail(fmt.Sprintf("panic: %v", r)).
                    WithCause(fmt.Errorf("panic: %v", r))
            }
        }()

        return handler(req)
    }
}

// ==================== Error Formatting ====================

// formatError formats an error into a friendly response
func formatError(err error) *Response {
    var appErr *AppError
    if errors.As(err, &appErr) {
        msg := fmt.Sprintf("Error [%d]: %s", appErr.Code, appErr.Message)
        if appErr.Detail != "" {
            msg += fmt.Sprintf("\n  Detail: %s", appErr.Detail)
        }
        if len(appErr.Context) > 0 {
            msg += "\n  Context:"
            for k, v := range appErr.Context {
                msg += fmt.Sprintf("\n    %s: %s", k, v)
            }
        }
        return &Response{
            StatusCode: appErr.Code,
            Body:       msg,
        }
    }

    return &Response{
        StatusCode: 500,
        Body:       fmt.Sprintf("Unknown error: %v", err),
    }
}

// ==================== Main Function ====================

func main() {
    // Wrap handler with Recovery middleware
    safeHandler := RecoveryMiddleware(handleRequest)

    // Test various scenarios
    testCases := []struct {
        name string
        req  *Request
    }{
        {
            name: "Normal request",
            req:  &Request{UserID: 1, Path: "/users", IsAdmin: true},
        },
        {
            name: "Invalid parameters",
            req:  &Request{UserID: -1, Path: "/users"},
        },
        {
            name: "Unauthenticated",
            req:  &Request{UserID: 0, Path: "/users"},
        },
        {
            name: "Insufficient permissions",
            req:  &Request{UserID: 2, Path: "/admin", IsAdmin: false},
        },
        {
            name: "Resource not found",
            req:  &Request{UserID: 1, Path: "/unknown"},
        },
        {
            name: "Trigger panic (auto-recovery)",
            req:  &Request{UserID: 1, Path: "/crash"},
        },
    }

    for _, tc := range testCases {
        fmt.Printf("=== %s ===\n", tc.name)

        resp, err := safeHandler(tc.req)
        if err != nil {
            resp = formatError(err)
        }

        fmt.Printf("  Status: %d\n", resp.StatusCode)
        fmt.Printf("  Response: %s\n\n", resp.Body)
    }
}
▶ Try it Yourself

Output:

=== Normal request ===
  Status: 200
  Response: User list

=== Invalid parameters ===
  Status: 400
  Response: Error [400]: bad request
  Detail: user_id must be a positive integer
  Context:
    user_id: -1

=== Unauthenticated ===
  Status: 401
  Response: Error [401]: unauthorized access
  Detail: missing authentication credentials

=== Insufficient permissions ===
  Status: 403
  Response: Error [403]: forbidden
  Detail: admin privileges required
  Context:
    user_id: 2

=== Resource not found ===
  Status: 404
  Response: Error [404]: resource not found
  Detail: resource for path /unknown not found

=== Trigger panic (auto-recovery) ===
[PANIC] runtime error: invalid memory address or nil pointer dereference
Stack:
...
  Status: 500
  Response: Error [500]: internal server error
  Detail: panic: runtime error: invalid memory address or nil pointer dereference

Real-World Scenarios

Scenario 1: Database Transaction Rollback

In database operations involving multiple steps, any failure requires rolling back already-executed operations.

GO
package main

import (
    "errors"
    "fmt"
)

var (
    ErrBalanceInsufficient = errors.New("insufficient balance")
    ErrAccountFrozen       = errors.New("account is frozen")
)

// TxError transaction error, records the failed step
type TxError struct {
    Step    string
    Cause   error
    Actions []string // Executed actions that need rollback
}

func (e *TxError) Error() string {
    return fmt.Sprintf("transaction failed [step: %s]: %v", e.Step, e.Cause)
}

func (e *TxError) Unwrap() error {
    return e.Cause
}

// TransferService transfer service
type TransferService struct {
    balances map[string]float64
    frozen   map[string]bool
}

// Transfer executes a transfer transaction
func (s *TransferService) Transfer(from, to string, amount float64) error {
    var executedSteps []string

    // Step 1: Validate source account
    if s.frozen[from] {
        return &TxError{
            Step:  "validate source account",
            Cause: ErrAccountFrozen,
        }
    }
    executedSteps = append(executedSteps, "lock source account")

    // Step 2: Check balance
    if s.balances[from] < amount {
        return &TxError{
            Step:    "check balance",
            Cause:   ErrBalanceInsufficient,
            Actions: executedSteps,
        }
    }
    executedSteps = append(executedSteps, "balance check passed")

    // Step 3: Deduct from source account
    s.balances[from] -= amount
    executedSteps = append(executedSteps, fmt.Sprintf("deducted %.2f", amount))

    // Step 4: Validate target account
    if s.frozen[to] {
        return &TxError{
            Step:    "validate target account",
            Cause:   ErrAccountFrozen,
            Actions: executedSteps, // Need to rollback deducted amount
        }
    }

    // Step 5: Add to target account
    s.balances[to] += amount

    fmt.Printf("  Transfer successful: %s -> %s, Amount: %.2f\n", from, to, amount)
    return nil
}

func main() {
    svc := &TransferService{
        balances: map[string]float64{
            "Alice": 1000,
            "Bob":   500,
            "Carol": 0,
        },
        frozen: map[string]bool{
            "Carol": true,
        },
    }

    tests := []struct {
        name string
        from string
        to   string
        amt  float64
    }{
        {"Normal transfer", "Alice", "Bob", 200},
        {"Insufficient balance", "Alice", "Bob", 9999},
        {"Target account frozen", "Alice", "Carol", 100},
    }

    for _, t := range tests {
        fmt.Printf("--- %s ---\n", t.name)
        fmt.Printf("  Before: %s=%.2f, %s=%.2f\n",
            t.from, svc.balances[t.from], t.to, svc.balances[t.to])

        err := svc.Transfer(t.from, t.to, t.amt)
        if err != nil {
            fmt.Printf("  Failed: %v\n", err)

            // Check if rollback is needed
            var txErr *TxError
            if errors.As(err, &txErr) && len(txErr.Actions) > 0 {
                fmt.Printf("  Rollback %d executed actions:\n", len(txErr.Actions))
                for _, action := range txErr.Actions {
                    fmt.Printf("    - Undo: %s\n", action)
                }
                // In real projects, execute rollback logic here
                // e.g.: svc.balances[from] += amount
            }
        }

        fmt.Printf("  After: %s=%.2f, %s=%.2f\n\n",
            t.from, svc.balances[t.from], t.to, svc.balances[t.to])
    }
}

Scenario 2: Batch File Processing with Error Collection

When processing multiple files, don't stop at the first failure. Instead, collect all errors and report them together.

GO
package main

import (
    "errors"
    "fmt"
    "strings"
)

var (
    ErrFileCorrupted = errors.New("file is corrupted")
    ErrPermission    = errors.New("insufficient permissions")
    ErrDiskFull      = errors.New("disk space insufficient")
)

// FileError error for a single file
type FileError struct {
    Path  string
    Op    string
    Cause error
}

func (e *FileError) Error() string {
    return fmt.Sprintf("[%s] %s: %v", e.Op, e.Path, e.Cause)
}

func (e *FileError) Unwrap() error {
    return e.Cause
}

// BatchResult batch processing result
type BatchResult struct {
    Success []string
    Failed  []FileError
}

func (r *BatchResult) HasErrors() bool {
    return len(r.Failed) > 0
}

func (r *BatchResult) Summary() string {
    var sb strings.Builder
    sb.WriteString(fmt.Sprintf("Processing complete: %d succeeded, %d failed\n",
        len(r.Success), len(r.Failed)))

    if len(r.Success) > 0 {
        sb.WriteString("Successful files:\n")
        for _, f := range r.Success {
            sb.WriteString(fmt.Sprintf("  ✓ %s\n", f))
        }
    }

    if len(r.Failed) > 0 {
        sb.WriteString("Failed files:\n")
        for _, e := range r.Failed {
            sb.WriteString(fmt.Sprintf("  ✗ %s (%s: %v)\n", e.Path, e.Op, e.Cause))
        }
    }

    return sb.String()
}

// processFile simulates processing a single file
func processFile(path string) error {
    // Simulate various possible errors
    fileErrors := map[string]error{
        "corrupted.txt": ErrFileCorrupted,
        "secret.txt":    ErrPermission,
        "huge.txt":      ErrDiskFull,
    }

    if err, exists := fileErrors[path]; exists {
        return &FileError{
            Path:  path,
            Op:    "process",
            Cause: err,
        }
    }
    return nil
}

// processFiles processes files in batch
func processFiles(paths []string) *BatchResult {
    result := &BatchResult{}

    for _, path := range paths {
        err := processFile(path)
        if err != nil {
            result.Failed = append(result.Failed, FileError{
                Path:  path,
                Op:    "process",
                Cause: errors.Unwrap(err),
            })
        } else {
            result.Success = append(result.Success, path)
        }
    }

    return result
}

func main() {
    files := []string{
        "report.pdf",
        "data.csv",
        "corrupted.txt",
        "image.png",
        "secret.txt",
        "huge.txt",
        "readme.md",
    }

    fmt.Printf("Starting to process %d files...\n\n", len(files))

    result := processFiles(files)
    fmt.Println(result.Summary())

    // Provide different suggestions based on error types
    for _, fe := range result.Failed {
        switch {
        case errors.Is(fe.Cause, ErrFileCorrupted):
            fmt.Printf("Suggestion: %s needs to be restored from backup\n", fe.Path)
        case errors.Is(fe.Cause, ErrPermission):
            fmt.Printf("Suggestion: Please check file permissions for %s\n", fe.Path)
        case errors.Is(fe.Cause, ErrDiskFull):
            fmt.Printf("Suggestion: Free up disk space and retry %s\n", fe.Path)
        }
    }
}

❓ FAQ

Q1: When should I use panic vs return error?

Principle: Use error for expected failures; use panic for programming errors that "should never happen."

GO
// ✓ Correct: return error — user input is unpredictable
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("divisor cannot be zero")
    }
    return a / b, nil
}

// ✓ Correct: use panic — this is a programmer's bug, nil should never be passed
func MustNotNil(v interface{}) {
    if v == nil {
        panic("value must not be nil, this is a caller's programming error")
    }
}

Practical rules:

Q2: What's the difference between errors.Is and errors.As?

GO
// errors.Is: checks if the error chain contains a specific sentinel error
// Equivalent to: err == target || err.Unwrap() == target || ...
errors.Is(err, ErrNotFound)  // true/false

// errors.As: extracts a specific error type from the error chain
// Like a type assertion, but traverses the entire error chain
var appErr *AppError
errors.As(err, &appErr)  // true + assignment, or false

// Analogy:
// errors.Is → "Are you Zhang San?" (checking identity)
// errors.As → "Are you a programmer? If so, tell me your skills" (extracting info)

Go official and community convention:

GO
// ✓ Recommended
fmt.Errorf("reading file %s failed: %w", name, err)
// Output: reading file config.yaml failed: permission denied

// ✗ Not recommended
fmt.Errorf("reading file %s failed.: %w", name, err)  // Unnecessary punctuation
fmt.Errorf("reading file %s failed!: %w", name, err)  // Unnecessary exclamation
fmt.Errorf("Read file %s failed: %w", name, err)      // Inconsistent language

The reason is errors are often concatenated into error chains, and lowercase without punctuation is easier to read:

initializing database: connecting to postgres: dial tcp 127.0.0.1:5432: connection refused

Q4: How do I compare wrapped errors?

Direct == comparison won't work for wrapped errors; you must use errors.Is:

GO
var ErrSentinel = errors.New("sentinel")

wrapped := fmt.Errorf("context: %w", ErrSentinel)

// ✗ Wrong: direct comparison fails
fmt.Println(wrapped == ErrSentinel)  // false

// ✓ Correct: use errors.Is
fmt.Println(errors.Is(wrapped, ErrSentinel))  // true

// errors.Is traverses the entire error chain:
// wrapped → Unwrap() → ErrSentinel → match!

📖 Summary

Topic Key Points
error interface Only needs to implement Error() string method
Creating errors errors.New for simple errors, fmt.Errorf + %w for wrapped errors
Checking errors errors.Is for type checking, errors.As for type extraction
Custom errors Implement Error() + Unwrap() methods, include business context
panic/recover Only for unrecoverable errors; catch with recover in defer
Error philosophy Explicit handling, check each error, errors are values not exceptions
Error wrapping Use %w to preserve underlying errors, use %v to discard them
Community convention Error messages lowercase without punctuation, as the last return value

Three golden rules of Go error handling:

  1. Check immediately — After receiving an error, check err != nil right away
  2. Wrap at each layer — Add context at each layer, use %w to preserve the original error
  3. Graceful degradation — Recover if possible, report up if not, panic only as a last resort

📝 Exercises

Exercise 1: Implement a File Reader with Retry

Write a function ReadWithRetry(path string, maxRetries int) ([]byte, error) with these requirements:

GO
// Reference framework
var ErrMaxRetriesExceeded = errors.New("max retries exceeded")

func ReadWithRetry(path string, maxRetries int) ([]byte, error) {
    var lastErr error
    for i := 0; i < maxRetries; i++ {
        data, err := os.ReadFile(path)
        if err == nil {
            return data, nil
        }
        lastErr = err
        fmt.Printf("  Retry %d failed: %v\n", i+1, err)
    }
    return nil, fmt.Errorf("reading %s: %w (last error: %v)", path, ErrMaxRetriesExceeded, lastErr)
}

Exercise 2: Build an Error Level System

Implement an error level system supporting the following error levels:

GO
type Level int

const (
    LevelDebug Level = iota
    LevelInfo
    LevelWarn
    LevelError
    LevelFatal
)

type LeveledError struct {
    Level   Level
    Message string
    Cause   error
}

Requirements:

Exercise 3: Implement an Error Collector (Concurrency Safe)

Write an ErrorCollector type for collecting errors from multiple goroutines in concurrent scenarios:

GO
type ErrorCollector struct {
    // Fields you need
}

// Add adds an error (concurrency safe)
func (c *ErrorCollector) Add(err error) { ... }

// Errors returns all collected errors
func (c *ErrorCollector) Errors() []error { ... }

// HasErrors checks if there are any errors
func (c *ErrorCollector) HasErrors() bool { ... }

// Error merges all errors into a single error
func (c *ErrorCollector) Error() error { ... }

Requirements:


Next Lesson

After completing this lesson, continue with Lesson 11: Packages and Modules to learn how to organize Go code, manage dependencies, and publish your own packages.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏