Go Error Types
Introduction
Error handling is a critical aspect of writing robust Go programs. Unlike many other programming languages that use exceptions, Go takes a different approach by making errors explicit values that must be checked and handled. This design philosophy encourages developers to think about possible failure scenarios and handle them appropriately.
In this guide, we'll explore the different types of errors in Go, how they work, and how to use them effectively in your programs. By the end, you'll have a solid understanding of Go's error handling mechanisms and be able to implement proper error handling in your own applications.
The Error Interface
At the heart of Go's error handling is the error interface. It's a built-in interface defined in the Go standard library as:
type error interface {
    Error() string
}
This simple interface only requires one method: Error(), which returns a string describing the error. Any type that implements this method satisfies the error interface and can be used as an error in Go.
Basic Error Types
1. Simple String Errors
The simplest way to create errors in Go is using the errors.New() function from the standard library:
package main
import (
    "errors"
    "fmt"
)
func main() {
    err := errors.New("something went wrong")
    
    if err != nil {
        fmt.Println(err)
    }
}
Output:
something went wrong
Another way to create simple errors is with fmt.Errorf(), which allows you to format error messages:
package main
import (
    "fmt"
)
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide %d by zero", a)
    }
    return a / b, nil
}
func main() {
    result, err := divide(10, 0)
    
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
}
Output:
Error: cannot divide 10 by zero
2. Custom Error Types
For more complex error handling, you can create custom error types by defining structures that implement the error interface:
package main
import (
    "fmt"
)
// Define a custom error type
type DivisionError struct {
    Dividend int
    Divisor  int
    Message  string
}
// Implement the Error method to satisfy the error interface
func (e *DivisionError) Error() string {
    return fmt.Sprintf("%s: %d / %d", e.Message, e.Dividend, e.Divisor)
}
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, &DivisionError{
            Dividend: a,
            Divisor:  b,
            Message:  "cannot divide by zero",
        }
    }
    return a / b, nil
}
func main() {
    result, err := divide(10, 0)
    
    if err != nil {
        fmt.Println("Error occurred:", err)
        
        // Type assertion to access custom error fields
        if divErr, ok := err.(*DivisionError); ok {
            fmt.Printf("Additional info - Dividend: %d, Divisor: %d
", 
                      divErr.Dividend, divErr.Divisor)
        }
    } else {
        fmt.Println("Result:", result)
    }
}
Output:
Error occurred: cannot divide by zero: 10 / 0
Additional info - Dividend: 10, Divisor: 0
Custom error types provide several advantages:
- They allow you to include additional context about the error
- They can be identified through type assertions
- They can include additional methods for error handling
Error Wrapping (Go 1.13+)
Starting with Go 1.13, the standard library added support for error wrapping, which allows errors to contain other errors. This is useful for adding context to errors while preserving the original error information.
Using fmt.Errorf with %w
The %w verb in fmt.Errorf() wraps an error with additional context:
package main
import (
    "errors"
    "fmt"
)
func readConfig(path string) error {
    // Simulate a file not found error
    err := errors.New("file not found")
    return fmt.Errorf("failed to read config from %s: %w", path, err)
}
func main() {
    err := readConfig("/etc/app/config.json")
    
    if err != nil {
        fmt.Println(err)
        
        // Unwrap to get the original error
        originalErr := errors.Unwrap(err)
        if originalErr != nil {
            fmt.Println("Original error:", originalErr)
        }
    }
}
Output:
failed to read config from /etc/app/config.json: file not found
Original error: file not found
Examining Wrapped Errors
Go provides two useful functions for working with wrapped errors:
- errors.Is()- Checks if an error or any error it wraps matches a target error
- errors.As()- Tries to find an error of a specific type in the error chain
Example using errors.Is():
package main
import (
    "errors"
    "fmt"
    "os"
)
func openFile(path string) error {
    err := os.ErrNotExist // Simulate a "file not found" error
    return fmt.Errorf("failed to open file %s: %w", path, err)
}
func main() {
    err := openFile("data.txt")
    
    if err != nil {
        fmt.Println(err)
        
        // Check if the error or any wrapped error is os.ErrNotExist
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("The file does not exist, creating it...")
            // Code to create the file would go here
        }
    }
}
Output:
failed to open file data.txt: file does not exist
The file does not exist, creating it...
Example using errors.As():
package main
import (
    "errors"
    "fmt"
    "os"
)
// Custom error type
type PathError struct {
    Path string
}
func (e *PathError) Error() string {
    return fmt.Sprintf("invalid path: %s", e.Path)
}
func validatePath(path string) error {
    // Simulate an invalid path
    pathErr := &PathError{Path: path}
    return fmt.Errorf("validation failed: %w", pathErr)
}
func main() {
    err := validatePath("/invalid/path")
    
    if err != nil {
        fmt.Println(err)
        
        // Try to extract a PathError from the error chain
        var pathErr *PathError
        if errors.As(err, &pathErr) {
            fmt.Printf("Path validation error for: %s
", pathErr.Path)
        }
    }
}
Output:
validation failed: invalid path: /invalid/path
Path validation error for: /invalid/path
Sentinel Errors
Go's standard library defines some predefined error values known as "sentinel errors." These are specific error values that can be checked using the equality operator (==) or errors.Is().
Common examples include:
- io.EOF- Indicates the end of a file or stream
- os.ErrNotExist- Indicates a file does not exist
- os.ErrPermission- Indicates permission issues
Example using a sentinel error:
package main
import (
    "fmt"
    "io"
    "strings"
)
func readUntilEOF(r io.Reader) error {
    buf := make([]byte, 1024)
    
    for {
        _, err := r.Read(buf)
        if err != nil {
            if err == io.EOF {
                // This is an expected error, not a failure
                fmt.Println("Reached the end of the input")
                return nil
            }
            // Any other error is unexpected
            return fmt.Errorf("error reading data: %w", err)
        }
        fmt.Println("Read some data")
    }
}
func main() {
    // Create a reader with a short string
    r := strings.NewReader("hello")
    
    err := readUntilEOF(r)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Operation completed successfully")
    }
}
Output:
Read some data
Reached the end of the input
Operation completed successfully
Error Groups (concurrent error handling)
When dealing with concurrent operations, the errgroup package from golang.org/x/sync provides a way to manage errors from multiple goroutines:
package main
import (
    "context"
    "fmt"
    "net/http"
    "time"
    
    "golang.org/x/sync/errgroup"
)
func checkWebsite(url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return fmt.Errorf("error fetching %s: %w", url, err)
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("bad status for %s: %d", url, resp.StatusCode)
    }
    
    return nil
}
func main() {
    // Create a list of websites to check
    websites := []string{
        "https://golang.org",
        "https://nonexistent-site-12345.com", // This will fail
        "https://github.com",
    }
    
    // Create a context with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    
    // Create an error group
    g, ctx := errgroup.WithContext(ctx)
    
    // Check each website concurrently
    for _, url := range websites {
        url := url // Create a new variable to avoid closure issues
        
        g.Go(func() error {
            return checkWebsite(url)
        })
    }
    
    // Wait for all goroutines to complete or for an error
    if err := g.Wait(); err != nil {
        fmt.Println("Error during website checks:", err)
    } else {
        fmt.Println("All websites checked successfully")
    }
}
Output:
Error during website checks: error fetching https://nonexistent-site-12345.com: Get "https://nonexistent-site-12345.com": dial tcp: lookup nonexistent-site-12345.com: no such host
Real-World Error Handling Patterns
1. Database Operations
Here's a practical example of error handling in a database operation:
package main
import (
    "database/sql"
    "errors"
    "fmt"
    
    _ "github.com/lib/pq" // PostgreSQL driver
)
// Custom error types for database operations
type NotFoundError struct {
    ID   int
    Type string
}
func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s with ID %d not found", e.Type, e.ID)
}
// User represents a user in our system
type User struct {
    ID   int
    Name string
}
// GetUserByID retrieves a user from the database
func GetUserByID(db *sql.DB, id int) (*User, error) {
    var user User
    
    err := db.QueryRow("SELECT id, name FROM users WHERE id = $1", id).Scan(&user.ID, &user.Name)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            // Wrap the standard error with our custom error
            return nil, &NotFoundError{ID: id, Type: "User"}
        }
        return nil, fmt.Errorf("database error retrieving user %d: %w", id, err)
    }
    
    return &user, nil
}
func main() {
    // In a real application, you would have a valid database connection
    // For this example, we'll simulate the error
    
    // Simulate the GetUserByID function with an error
    user, err := func(id int) (*User, error) {
        return nil, &NotFoundError{ID: id, Type: "User"}
    }(42)
    
    if err != nil {
        fmt.Println("Error:", err)
        
        // Check if it's a NotFoundError
        var notFoundErr *NotFoundError
        if errors.As(err, ¬FoundErr) {
            fmt.Printf("Please check if the user ID %d is correct
", notFoundErr.ID)
        }
    } else {
        fmt.Printf("Found user: %s (ID: %d)
", user.Name, user.ID)
    }
}
Output:
Error: User with ID 42 not found
Please check if the user ID 42 is correct
2. API Response Errors
When building REST APIs, it's common to map errors to appropriate HTTP responses:
package main
import (
    "encoding/json"
    "errors"
    "fmt"
    "log"
    "net/http"
)
// API error types
type NotFoundError struct {
    Resource string
    ID       string
}
func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s with ID %s not found", e.Resource, e.ID)
}
type ValidationError struct {
    Field   string
    Message string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error on field %s: %s", e.Field, e.Message)
}
// Simulated service function
func getProduct(id string) (map[string]interface{}, error) {
    // Simulate a product not found error
    if id == "999" {
        return nil, &NotFoundError{Resource: "Product", ID: id}
    }
    
    // Simulate a validation error
    if id == "invalid" {
        return nil, &ValidationError{Field: "id", Message: "product ID must be numeric"}
    }
    
    // Success case
    return map[string]interface{}{
        "id":    id,
        "name":  "Sample Product",
        "price": 29.99,
    }, nil
}
// HTTP handler
func productHandler(w http.ResponseWriter, r *http.Request) {
    // Get the product ID from the URL query parameters
    id := r.URL.Query().Get("id")
    if id == "" {
        http.Error(w, "missing product ID", http.StatusBadRequest)
        return
    }
    
    // Try to get the product
    product, err := getProduct(id)
    if err != nil {
        // Handle different error types
        var notFoundErr *NotFoundError
        var validationErr *ValidationError
        
        switch {
        case errors.As(err, ¬FoundErr):
            // Return 404 Not Found for missing resources
            w.WriteHeader(http.StatusNotFound)
            json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
            
        case errors.As(err, &validationErr):
            // Return 400 Bad Request for validation errors
            w.WriteHeader(http.StatusBadRequest)
            json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
            
        default:
            // Log unexpected errors and return 500 Internal Server Error
            log.Printf("Unexpected error: %v", err)
            w.WriteHeader(http.StatusInternalServerError)
            json.NewEncoder(w).Encode(map[string]string{"error": "internal server error"})
        }
        return
    }
    
    // Success case - return the product
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(product)
}
func main() {
    // For demonstration, we'll simulate API responses
    
    // Simulate a successful request
    fmt.Println("Response for valid product (ID: 123):")
    product, _ := getProduct("123")
    productJSON, _ := json.MarshalIndent(product, "", "  ")
    fmt.Printf("HTTP 200 OK
%s
", productJSON)
    
    // Simulate a not found error
    fmt.Println("Response for non-existent product (ID: 999):")
    _, err := getProduct("999")
    if err != nil {
        fmt.Printf("HTTP 404 Not Found
{\"error\": \"%s\"}
", err.Error())
    }
    
    // Simulate a validation error
    fmt.Println("Response for invalid product ID:")
    _, err = getProduct("invalid")
    if err != nil {
        fmt.Printf("HTTP 400 Bad Request
{\"error\": \"%s\"}
", err.Error())
    }
}
Output:
Response for valid product (ID: 123):
HTTP 200 OK
{
  "id": "123",
  "name": "Sample Product",
  "price": 29.99
}
Response for non-existent product (ID: 999):
HTTP 404 Not Found
{"error": "Product with ID 999 not found"}
Response for invalid product ID:
HTTP 400 Bad Request
{"error": "validation error on field id: product ID must be numeric"}
Error Handling Best Practices
When working with errors in Go, keep these best practices in mind:
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!