GoLang – 10 – Error Handling Best Practices

Error Handling Best Practices in Go: Error Wrapping and Context, Custom Error Types

Error handling is a critical aspect of writing robust and maintainable Go code. In this section, we’ll explore two key best practices for effective error handling in Go: error wrapping and context, and the creation of custom error types.

Error Wrapping and Context

Error wrapping and context are important techniques for preserving useful error information while passing it up the call stack. These practices help you identify the root cause of an error and provide valuable context when debugging or logging errors.

Error Wrapping

Wrapping an error involves adding additional information to the error message, making it more informative. In Go, you can use the fmt.Errorf function to wrap an error with a custom message:


import (
    "fmt"
    "errors"
)

func openFile(filename string) error {
    _, err := openFileImpl(filename)
    if err != nil {
        return fmt.Errorf("failed to open file: %w", err)
    }
    return nil
}

func openFileImpl(filename string) (*file, error) {
    // Implementation to open a file
    if err != nil {
        return nil, errors.New("file not found")
    }
    return file, nil
}

In this example, we wrap the error returned by openFileImpl with a custom message using fmt.Errorf. The %w verb indicates that the returned error is wrapped with the previous error.

Error Context

Adding context to an error means including relevant information about the context in which the error occurred. You can use the errors.New function and the fmt.Errorf function to add context:


import (
    "fmt"
    "errors"
)

func processFile(filename string) error {
    data, err := readFile(filename)
    if err != nil {
        return fmt.Errorf("failed to process file %s: %w", filename, err)
    }
    // Process data
    return nil
}

func readFile(filename string) ([]byte, error) {
    // Implementation to read a file
    if err != nil {
        return nil, errors.New("file not found")
    }
    return data, nil
}

In this code, we provide context by including the filename in the error message using fmt.Errorf. It makes it easier to understand where the error occurred.

Custom Error Types

Creating custom error types is a good practice to handle specific error conditions and make error handling more structured and organized. Custom error types allow you to define error behavior, and they can be checked using type assertions.

Defining Custom Error Types

Define custom error types by implementing the error interface in your own types:


type FileError struct {
    Op  string
    Err error
}

func (e *FileError) Error() string {
    return e.Op + ": " + e.Err.Error()
}

func readFile(filename string) ([]byte, error) {
    // Implementation to read a file
    if err != nil {
        return nil, &FileError{"readFile", errors.New("file not found")}
    }
    return data, nil
}

In this example, we define a custom error type FileError with fields for the operation and the underlying error. We implement the Error method to provide a formatted error message.

Using Custom Error Types

You can use custom error types to improve error handling and make it more context-aware:


func processFile(filename string) error {
    data, err := readFile(filename)
    if err != nil {
        if fileErr, ok := err.(*FileError); ok {
            if fileErr.Err == os.ErrNotExist {
                // Handle file not found
            }
        }
        return err
    }
    // Process data
    return nil
}

In this code, we use type assertions to check if the error is of type *FileError. If it is, we can access the specific details of the custom error type, such as the underlying error and the operation.

By following best practices like error wrapping and context and creating custom error types, you can improve the error handling capabilities of your Go code. These practices make your error messages more informative and enhance your code’s ability to handle and respond to errors effectively.