Error Handling in Go

Introduction to Error Handling

In Go, error handling is explicit and is often done through returning error values from functions. Unlike many other languages that rely on exceptions, Go uses a more manual but explicit approach to handle errors, which helps to make error handling clear and predictable.

Go's Error Type

Go has a built-in error type, which is a simple interface with a single method:

type error interface {
    Error() string
}

The Error() method returns the error message as a string.

Returning Errors

Most functions that can fail return an error as the last return value. You can check if the error is nil to determine if the function succeeded.

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(4, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
}

In this example, the divide function returns an error if the divisor is zero. The caller checks for an error and handles it accordingly.

Custom Error Types

You can create custom error types by defining a type that implements the error interface. This allows you to attach more context to the error.

package main

import (
    "fmt"
)

type DivisionError struct {
    Dividend, Divisor int
}

func (e *DivisionError) 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, &DivisionError{a, b}
    }
    return a / b, nil
}

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

In this example, we create a custom error type DivisionError that holds the dividend and divisor. When a division by zero occurs, the function returns this custom error.

Wrapping Errors

Go 1.13 introduced error wrapping, allowing you to wrap errors with additional context while retaining the original error. You can use fmt.Errorf with the %w verb to wrap errors.

package main

import (
    "fmt"
    "errors"
)

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("division failed: %w", errors.New("cannot divide by zero"))
    }
    return a / b, nil
}

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

Here, we use fmt.Errorf to wrap the error with additional context. The %w verb preserves the original error, allowing you to inspect it later using errors.Is or errors.As.

Conclusion

Go's explicit error handling makes it easy to deal with issues in a predictable way. By returning error values and checking them manually, your program becomes more robust and transparent. In the next tutorial, we will explore Go's powerful features for handling concurrency.