Module: Error Handling

Custom Errors

Go Programming: Error Handling - Custom Errors

Go's error handling relies heavily on returning an error interface. While the standard errors package provides helpful functions for creating basic errors, often you'll need to define custom errors to provide more context and specific information about what went wrong in your application. This document outlines how to create and use custom errors in Go.

Why Custom Errors?

  • Specificity: Standard errors like io.EOF or os.ErrNotExist are useful, but they might not fully capture the nuances of an error within your application logic.
  • Context: Custom errors can carry additional data relevant to the error, aiding in debugging and recovery.
  • Error Identification: You can use custom error types to specifically identify and handle certain error conditions in your code.
  • Testability: Custom errors make it easier to write unit tests that verify specific error scenarios.

Methods for Creating Custom Errors

There are several ways to create custom errors in Go:

1. Using errors.New() with a descriptive message:

This is the simplest approach for basic custom errors.

import "errors"

var ErrInvalidInput = errors.New("invalid input provided")

func processInput(input string) error {
	if input == "" {
		return ErrInvalidInput
	}
	// ... process input ...
	return nil
}

func main() {
	err := processInput("")
	if errors.Is(err, ErrInvalidInput) { // Use errors.Is for comparison
		println("Input is invalid")
	} else if err != nil {
		println("An unexpected error occurred:", err)
	}
}
  • Pros: Simple, easy to understand.
  • Cons: Limited to a string message. No way to attach additional data. Error comparison relies on string matching (less robust).

2. Defining a Custom Error Type (Struct):

This is the most flexible and recommended approach. You define a struct that implements the error interface.

import "fmt"

type MyCustomError struct {
	Code    int
	Message string
	Details string // Optional: Additional error details
}

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

func doSomething(value int) error {
	if value < 0 {
		return &MyCustomError{
			Code:    1001,
			Message: "Value must be non-negative",
			Details: "The provided value was less than zero.",
		}
	}
	// ... do something with value ...
	return nil
}

func main() {
	err := doSomething(-5)
	if err != nil {
		// Type assertion to access the custom error fields
		if customErr, ok := err.(*MyCustomError); ok {
			fmt.Println("Custom Error Code:", customErr.Code)
			fmt.Println("Custom Error Message:", customErr.Message)
			fmt.Println("Custom Error Details:", customErr.Details)
		} else {
			fmt.Println("An unexpected error occurred:", err)
		}
	}
}
  • Pros: Allows you to attach arbitrary data to the error. Provides a structured way to represent error information. More robust error comparison using type assertions.
  • Cons: More verbose than errors.New().

3. Defining a Custom Error Type (Type Alias):

This is a simpler variation of the struct approach, useful when you just need to add a type to an existing error.

import "fmt"

type ValidationError error // Type alias

func validateData(data string) ValidationError {
	if len(data) == 0 {
		return fmt.Errorf("data is empty") // Return a ValidationError
	}
	return nil
}

func main() {
	err := validateData("")
	if err != nil {
		fmt.Println("Validation Error:", err)
	}
}
  • Pros: Concise, easy to use when you want to add a specific type to an existing error.
  • Cons: Doesn't allow adding custom fields like the struct approach.

4. Using fmt.Errorf() with %w for Wrapping Errors:

This is crucial for preserving the original error context. The %w verb wraps another error, allowing you to chain errors together.

import (
	"fmt"
	"errors"
)

func readFile(filename string) error {
	// Simulate file reading error
	return errors.New("file not found")
}

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

func main() {
	err := processFile("my_file.txt")
	if err != nil {
		fmt.Println("Error:", err)

		// Unwrapping the error to get the original error
		if errors.Is(err, errors.New("file not found")) {
			fmt.Println("File not found error detected!")
		}
	}
}
  • Pros: Preserves the original error context. Allows you to add more information to the error without losing the original cause. errors.Is and errors.As can be used to unwrap and inspect the error chain.
  • Cons: Requires understanding of error wrapping and unwrapping.

Best Practices

  • Use descriptive error messages: Make it clear what went wrong.
  • Include relevant context: Add information that will help with debugging.
  • Wrap errors: Use fmt.Errorf("%w", originalError) to preserve the original error context.
  • Use errors.Is for error comparison: Avoid string matching. errors.Is checks if an error is in the error chain.
  • Use errors.As to extract specific error types: Allows you to type-assert and access custom error fields.
  • Document your custom error types: Explain what each error represents and when it might occur.
  • Consider using error codes: Numeric codes can be useful for programmatic error handling.

errors.Is and errors.As

These functions, introduced in Go 1.13, are essential for working with wrapped errors.

  • errors.Is(err, target): Checks if err or any error in its chain is equal to target. This is the preferred way to compare errors.
  • errors.As(err, &target): Walks the error chain and finds the first error that matches the type of target. If found, it sets target to that error and returns true. This allows you to extract a specific error type from the chain.

These functions are much more reliable than comparing error messages directly, especially when errors are wrapped. They ensure that you're comparing the underlying error type, not just a string representation.

By following these guidelines, you can create robust and informative error handling in your Go applications. Custom errors are a powerful tool for building maintainable and reliable software.