Skip to main content
โšก Calmops

Error Handling in Go

Error Handling in Go

Error handling is a core part of Go programming. Go uses explicit error returns rather than exceptions, making error handling clear and intentional.

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.

Comments