Go Error Handling Essentials: A Beginner’s Guide

Go Error Handling Essentials: A Beginner’s Guide

1. The Basics: Errors in Go

What is an Error?

In Go, an error is a value indicating that something went wrong. Think of it as a signal saying, "Oops, something didn't go as planned!"

Basic Error Handling

Functions in Go often return two values: the result and an error. For example:

package main

import (
    "fmt"
    "errors"
)

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
}
  • divide(10, 0) returns an error because you can't divide by zero.

  • if err != nil checks if there's an error and handles it.

2. Custom Errors

Sometimes, you need more detailed error messages. You can create custom errors using fmt.Errorf.

package main

import (
    "fmt"
)

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("divide: cannot divide %d by %d", a, b)
    }
    return a / b, nil
}

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

3. The Error Interface

In Go, the error interface is a built-in type that represents an error condition. It is defined as:

type error interface {
    Error() string
}

Any type that implements the Error method can be used as an error. This allows you to create custom error types.

4. Custom Error Types

For more advanced error handling, you can define your own error types.

package main

import (
    "fmt"
)

type DivideError struct {
    Dividend int
    Divisor  int
}

func (e *DivideError) Error() string {
    return fmt.Sprintf("cannot divide %d by %d", e.Dividend, e.Divisor)
}

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, &DivideError{a, b}
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        if divErr, ok := err.(*DivideError); ok {
            fmt.Println("Divide error:", divErr)
        } else {
            fmt.Println("Error:", err)
        }
    } else {
        fmt.Println("Result:", result)
    }
}
  • DivideError is a custom error type.

  • It implements the Error method from the error interface.

5. Wrapping Errors

Go 1.13 introduced error wrapping, allowing you to retain the original error context.

package main

import (
    "fmt"
    "errors"
)

func openFile(filename string) error {
    return fmt.Errorf("openFile: %w", errors.New("file not found"))
}

func main() {
    err := openFile("test.txt")
    if err != nil {
        fmt.Println("Error:", err)
    }
}
  • %w is used to wrap the original error.

  • You can then use errors.Unwrap to retrieve the original error.

package main

import (
    "fmt"
    "errors"
)

func openFile(filename string) error {
    originalErr := errors.New("file not found")
    return fmt.Errorf("openFile: %w", originalErr)
}

func main() {
    err := openFile("test.txt")
    if err != nil {
        fmt.Println("Error:", err)
        unwrappedErr := errors.Unwrap(err)
        fmt.Println("Unwrapped Error:", unwrappedErr)
    }
}

// Error: openFile: file not found
// Unwrapped Error: file not found

6. Error Chaining

You can chain errors to add more context at different levels of your program. Here's an example with three levels of chaining:

package main

import (
    "fmt"
    "errors"
)

func readFile() error {
    return errors.New("readFile: file not found")
}

func parseData() error {
    err := readFile()
    if err != nil {
        return fmt.Errorf("parseData: %w", err)
    }
    return nil
}

func processData() error {
    err := parseData()
    if err != nil {
        return fmt.Errorf("processData: %w", err)
    }
    return nil
}

func main() {
    err := processData()
    if err != nil {
        fmt.Println("Error:", err)
        unwrappedErr := errors.Unwrap(err)
        fmt.Println("Unwrapped Error:", unwrappedErr)
        secondUnwrap := errors.Unwrap(unwrappedErr)
        fmt.Println("Second Unwrapped Error:", secondUnwrap)
    }
}
// Error: processData: parseData: readFile: file not found
// Unwrapped Error: parseData: readFile: file not found
// Second Unwrapped Error: readFile: file not found

7. Checking Error Types: errors.Is vs errors.As

errors.Is

errors.Is checks if an error matches a specific error value. This is useful for comparing against sentinel errors.

package main

import (
    "fmt"
    "errors"
)

var ErrDivideByZero = errors.New("cannot divide by zero")

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, ErrDivideByZero
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if errors.Is(err, ErrDivideByZero) {
        fmt.Println("Error: cannot divide by zero")
    } else {
        fmt.Println("Result:", result)
    }
}

errors.As

errors.As checks if an error can be cast to a specific type. This is useful for working with custom error types.

package main

import (
    "fmt"
    "errors"
)

type DivideError struct {
    Dividend int
    Divisor  int
}

func (e *DivideError) Error() string {
    return fmt.Sprintf("cannot divide %d by %d", e.Dividend, e.Divisor)
}

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, &DivideError{a, b}
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        var divErr *DivideError
        if errors.As(err, &divErr) {
            fmt.Printf("Divide error: %v (Dividend: %d, Divisor: %d)\n", divErr, divErr.Dividend, divErr.Divisor)
        } else {
            fmt.Println("Error:", err)
        }
    } else {
        fmt.Println("Result:", result)
    }
}