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.EOForos.ErrNotExistare 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.Isanderrors.Ascan 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.Isfor error comparison: Avoid string matching.errors.Ischecks if an error is in the error chain. - Use
errors.Asto 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 iferror any error in its chain is equal totarget. 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 oftarget. If found, it setstargetto that error and returnstrue. 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.