Learn › Go Programming

Error Handling

Master Go's explicit error handling approach with the error interface, custom error types, wrapping, and sentinel errors.

The Error Interface

Go handles errors through explicit return values rather than exceptions, using the built-in 'error' interface which requires only a single method: Error() string. Functions that can fail conventionally return an error as their last return value, and callers are expected to check this value immediately. This approach makes error handling visible in the code flow and avoids the hidden control flow that try-catch blocks introduce. The 'errors.New' function and 'fmt.Errorf' are the simplest ways to create error values.

package main

import (
    "errors"
    "fmt"
    "strconv"
)

func parseAge(s string) (int, error) {
    age, err := strconv.Atoi(s)
    if err != nil {
        return 0, fmt.Errorf("invalid age %q: %w", s, err)
    }
    if age < 0 || age > 150 {
        return 0, errors.New("age must be between 0 and 150")
    }
    return age, nil
}

func main() {
    age, err := parseAge("25")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Age:", age)

    _, err = parseAge("abc")
    if err != nil {
        fmt.Println("Error:", err)
    }

    _, err = parseAge("-5")
    if err != nil {
        fmt.Println("Error:", err)
    }
}

Custom Error Types

For richer error information, you can create custom error types by implementing the error interface on your own structs. Custom error types can carry additional context such as error codes, field names, or the operation that failed. This allows callers to use type assertions to extract detailed error information for specialized handling. Custom error types are especially useful in libraries where consumers need to make decisions based on the kind of error.

package main

import "fmt"

type ValidationError struct {
    Field   string
    Message string
}

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

type NotFoundError struct {
    Resource string
    ID       int
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s with ID %d not found", e.Resource, e.ID)
}

func findUser(id int) (string, error) {
    if id <= 0 {
        return "", &ValidationError{Field: "id", Message: "must be positive"}
    }
    if id > 100 {
        return "", &NotFoundError{Resource: "User", ID: id}
    }
    return "Alice", nil
}

func main() {
    _, err := findUser(200)
    if err != nil {
        switch e := err.(type) {
        case *ValidationError:
            fmt.Printf("Validation: field=%s msg=%s\n", e.Field, e.Message)
        case *NotFoundError:
            fmt.Printf("Not found: %s #%d\n", e.Resource, e.ID)
        default:
            fmt.Println("Unknown error:", err)
        }
    }
}

Error Wrapping

Go 1.13 introduced error wrapping with the '%w' verb in fmt.Errorf, allowing you to add context to errors while preserving the original error chain. The errors.Is function checks whether any error in the chain matches a target value, while errors.As checks whether any error in the chain matches a target type. This wrapping mechanism replaces the need for third-party error packages and enables clean, layered error handling across application boundaries.

package main

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

type DatabaseError struct {
    Query string
    Err   error
}

func (e *DatabaseError) Error() string {
    return fmt.Sprintf("database error for query %q: %v", e.Query, e.Err)
}

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

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

func main() {
    _, err := readConfig("/nonexistent/config.yaml")
    if err != nil {
        fmt.Println("Error:", err)

        // Check if the underlying error is a path error
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("File does not exist")
        }

        var pathErr *os.PathError
        if errors.As(err, &pathErr) {
            fmt.Println("Path:", pathErr.Path)
        }
    }
}

Sentinel Errors

Sentinel errors are predefined error values that represent specific error conditions and are compared using errors.Is. They are typically declared as package-level variables using errors.New and named with an 'Err' prefix by convention. Sentinel errors provide a stable API for error checking across package boundaries without exposing internal implementation details. Common examples in the standard library include io.EOF, sql.ErrNoRows, and os.ErrNotExist.

package main

import (
    "errors"
    "fmt"
)

var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrForbidden    = errors.New("forbidden")
)

type APIError struct {
    StatusCode int
    Err        error
}

func (e *APIError) Error() string {
    return fmt.Sprintf("API error %d: %v", e.StatusCode, e.Err)
}

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

func fetchResource(authenticated bool) error {
    if !authenticated {
        return &APIError{StatusCode: 401, Err: ErrUnauthorized}
    }
    return &APIError{StatusCode: 404, Err: ErrNotFound}
}

func main() {
    err := fetchResource(false)

    if errors.Is(err, ErrUnauthorized) {
        fmt.Println("Please log in first")
    } else if errors.Is(err, ErrNotFound) {
        fmt.Println("Resource does not exist")
    }

    var apiErr *APIError
    if errors.As(err, &apiErr) {
        fmt.Printf("Status code: %d\n", apiErr.StatusCode)
    }
}

← Structs & Interfaces · Goroutines & Channels →