Error Handling Patterns in Go: Beyond the Basics
Go's approach to error handling is often criticized by developers coming from other languages, but once you understand the patterns and idioms, it becomes a powerful tool for building robust applications.
The Foundation: Error Interface
Go's error interface is beautifully simple:
type error interface {
Error() string
}
This simplicity is both a strength and a challenge. While it's easy to implement, it doesn't provide structured information about what went wrong.
Custom Error Types
For more sophisticated error handling, custom error types are essential:
type ValidationError struct {
Field string
Message string
Code string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("validation failed for %s: %s", e.Field, e.Message)
}
Error Wrapping and Unwrapping
Go 1.13 introduced error wrapping, which allows you to add context while preserving the original error:
if err != nil {
return fmt.Errorf("failed to process user %d: %w", userID, err)
}
This creates a chain of errors that can be unwrapped using `errors.Unwrap()` or checked using `errors.Is()` and `errors.As()`.
Structured Error Responses
In web applications, I often use structured error responses:
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
Details map[string]interface{} `json:"details,omitempty"`
}
func (e APIError) Error() string {
return e.Message
}
Best Practices
- **Be Specific**: Use custom error types to provide structured information
- **Add Context**: Wrap errors with additional context as they bubble up
- **Handle at the Right Level**: Don't handle errors too early; let them bubble up to where they can be properly addressed
- **Log Appropriately**: Log errors at the boundary where they're handled, not everywhere they're returned
Error handling in Go requires discipline, but when done right, it leads to more robust and maintainable code.