Skip to main content

Error Handling in Go

Created: December 17, 2025 4 min read

Error handling is a core part of Go programming. Go uses explicit error returns rather than exceptions, making error handling clear and intentional. For more context, see Go Installation Guide, Go Ecosystem Overview, Go Best Practices.

Error Interface

Understanding the Error Interface

package main

import "fmt"

// Error interface definition
type error interface {
    Error() string
}

// Using built-in errors
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
}

Error Checking Patterns

Basic Error Checking

package main

import (
    "fmt"
    "os"
)

func main() {
    // Check error immediately
    file, err := os.Open("file.txt")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer file.Close()

    // Use file...
}

Multiple Error Checks

package main

import (
    "fmt"
    "io/ioutil"
    "os"
)

func main() {
    // Read file
    data, err := ioutil.ReadFile("file.txt")
    if err != nil {
        fmt.Println("Read error:", err)
        return
    }

    // Write file
    err = ioutil.WriteFile("output.txt", data, 0644)
    if err != nil {
        fmt.Println("Write error:", err)
        return
    }

    fmt.Println("Success")
}

Custom Errors

Creating Custom Error Types

package main

import (
    "fmt"
)

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

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

func validateEmail(email string) error {
    if email == "" {
        return &ValidationError{
            Field:   "email",
            Message: "email cannot be empty",
        }
    }
    return nil
}

func main() {
    err := validateEmail("")
    if err != nil {
        fmt.Println(err)
    }
}

Error with Context

package main

import (
    "fmt"
)

type AppError struct {
    Code    int
    Message string
    Err     error
}

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

func main() {
    err := &AppError{
        Code:    500,
        Message: "Internal Server Error",
        Err:     fmt.Errorf("database connection failed"),
    }
    fmt.Println(err)
}

Error Wrapping

Using fmt.Errorf

package main

import (
    "fmt"
    "os"
)

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

func main() {
    _, err := readConfig("config.json")
    if err != nil {
        fmt.Println(err)
    }
}

Unwrapping Errors

package main

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

func main() {
    _, err := os.Open("nonexistent.txt")
    if err != nil {
        // Check if it's a specific error type
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("File not found")
        }

        // Get underlying error
        var pathErr *os.PathError
        if errors.As(err, &pathErr) {
            fmt.Printf("Path: %s\n", pathErr.Path)
        }
    }
}

Error Handling Patterns

Early Return Pattern

package main

import (
    "fmt"
)

func processData(data string) error {
    if data == "" {
        return fmt.Errorf("data cannot be empty")
    }

    if len(data) > 100 {
        return fmt.Errorf("data too long")
    }

    // Process data...
    return nil
}

func main() {
    err := processData("test")
    if err != nil {
        fmt.Println("Error:", err)
    }
}

Defer for Cleanup

package main

import (
    "fmt"
    "os"
)

func processFile(path string) error {
    file, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("failed to open file: %w", err)
    }
    defer file.Close()

    // Process file...
    return nil
}

func main() {
    err := processFile("data.txt")
    if err != nil {
        fmt.Println(err)
    }
}

Panic and Recover

package main

import (
    "fmt"
)

func safeDivide(a, b int) (result int) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
            result = 0
        }
    }()

    if b == 0 {
        panic("division by zero")
    }
    return a / b
}

func main() {
    fmt.Println(safeDivide(10, 2))  // 5
    fmt.Println(safeDivide(10, 0))  // 0 (recovered)
}

Best Practices

✅ Good: Explicit Error Handling

// DO: Check errors explicitly
if err != nil {
    return fmt.Errorf("operation failed: %w", err)
}

❌ Bad: Ignoring Errors

// DON'T: Ignore errors
_ = someFunction()

✅ Good: Wrap Errors with Context

// DO: Add context when wrapping errors
if err != nil {
    return fmt.Errorf("failed to read config from %s: %w", path, err)
}

✅ Good: Use Sentinel Errors

// DO: Define sentinel errors for comparison
var ErrNotFound = errors.New("not found")
var ErrInvalid = errors.New("invalid input")

❌ Bad: Generic Error Messages

// DON'T: Use vague error messages
return fmt.Errorf("error")

Summary

Go’s error handling provides:

  1. Explicit error returns for clarity
  2. Error interface for custom errors
  3. Error wrapping for context
  4. Error checking patterns for safety
  5. Panic/recover for exceptional cases

These features make error handling explicit and intentional in Go programs.

Resources

Comments

Share this article

Scan to read on mobile