Rez Moss

Rez Moss

Personal Musings: A Blog for the Tech-Savvy and Curious Mind

Error Handling in Go: Best Practices and Patterns

Sep 2019

Error handling is a crucial aspect of writing robust and reliable Go programs. Unlike many other languages, Go takes a unique approach to error handling, treating errors as values rather than using exceptions. This article explores best practices and common patterns for effective error handling in Go.

The Basics: Returning Errors

In Go, functions that can fail often return an error as their last return value. The idiomatic way to handle these errors is to check them immediately after the function call.

result, err := someFunction()
if err != nil {
    // Handle the error
    return err
}
// Use result

This pattern encourages developers to consider error cases explicitly, leading to more reliable code.

Creating Custom Errors

While the built-in errors.New() function is sufficient for simple cases, creating custom error types can provide more context and enable better error handling.

type MyError struct {
    Code    int
    Message string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("error %d: %s", e.Code, e.Message)
}

Custom errors allow you to add fields that provide additional information about the error, making debugging easier.

The errors.Is() and errors.As() Functions

Go 1.13 introduced errors.Is() and errors.As(), which provide improved error checking capabilities, especially when dealing with wrapped errors.

if errors.Is(err, os.ErrNotExist) {
    // Handle file not found error
}

var myErr *MyError
if errors.As(err, &myErr) {
    // Handle MyError specifically
    fmt.Println(myErr.Code)
}

These functions make it easier to check for specific error types or values, even when errors are wrapped.

Error Wrapping

Error wrapping allows you to add context to an error without losing the original error information. Use the %w verb with fmt.Errorf() to wrap errors:

if err != nil {
    return fmt.Errorf("failed to process data: %w", err)
}

This pattern helps in creating error chains that provide a trail of what went wrong, making debugging easier.

Avoid Using panic()

In Go, panic() should be used only for unrecoverable errors. For most error cases, returning an error value is preferred. This allows the caller to decide how to handle the error.

// Avoid this
func someFunction() {
    if someCondition {
        panic("something went wrong")
    }
}

// Prefer this
func someFunction() error {
    if someCondition {
        return errors.New("something went wrong")
    }
    return nil
}

Using defer for Cleanup

When dealing with resources that need to be closed or cleaned up, use defer in combination with error checking:

file, err := os.Open("file.txt")
if err != nil {
    return err
}
defer file.Close()

// Rest of the function

This ensures that resources are properly released, even if an error occurs later in the function.

Effective error handling in Go involves consistently checking for errors, providing context through error wrapping, and creating custom error types when necessary. By following these practices and patterns, you can write Go code that is more robust, easier to debug, and maintainable in the long run. Remember, good error handling is not just about catching errors, but about making your code resilient and informative when things go wrong.