Error Handling Best Practices in Go
Error handling is a critical aspect of writing robust Go applications. This guide covers idiomatic approaches and best practices for handling errors effectively.
Understanding Go's Error Philosophy
Go's approach to error handling is distinct from many other languages:
Explicit Error Handling:
- Errors are values, not exceptions
- Errors must be checked explicitly
- No implicit error propagation
- Clear error flow in code
Error as Values:
- Errors are just interfaces
- Can contain rich context
- Can be programmatically examined
- Can be wrapped and unwrapped
Multiple Return Values:
- Functions return error as last value
- Separate success and failure paths
- Clear error propagation
- Easy to handle errors locally
Basic Error Handling
1. The Error Interface
The foundation of Go's error handling:
type error interface {
Error() string
}
Creating custom errors:
// Simple string error
errors.New("something went wrong")
// Formatted error
fmt.Errorf("failed to process %s: %w", name, err)
// Custom error type
type ValidationError struct {
Field string
Error string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Error)
}
2. Error Checking Patterns
Idiomatic error handling:
// Basic error check
result, err := SomeFunction()
if err != nil {
return err
}
// Multiple error checks
result, err := SomeFunction()
if err != nil {
return fmt.Errorf("failed to process: %w", err)
}
data, err := AnotherFunction(result)
if err != nil {
return fmt.Errorf("failed to handle result: %w", err)
}
Advanced Error Handling
1. Error Wrapping
Go 1.13+ error wrapping:
// Wrapping errors
func ProcessFile(path string) error {
data, err := readFile(path)
if err != nil {
return fmt.Errorf("failed to read file %s: %w", path, err)
}
if err := processData(data); err != nil {
return fmt.Errorf("failed to process data: %w", err)
}
return nil
}
// Unwrapping errors
if errors.Is(err, fs.ErrNotExist) {
// Handle file not found
}
var validationErr *ValidationError
if errors.As(err, &validationErr) {
// Handle validation error
}
2. Custom Error Types
Rich error types:
type BusinessError struct {
Code string
Message string
Details map[string]interface{}
err error
}
func (e *BusinessError) Error() string {
if e.err != nil {
return fmt.Sprintf("%s: %s (%v)", e.Code, e.Message, e.err)
}
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
func (e *BusinessError) Unwrap() error {
return e.err
}
// Usage
func ProcessOrder(order Order) error {
if err := validateOrder(order); err != nil {
return &BusinessError{
Code: "INVALID_ORDER",
Message: "Order validation failed",
Details: map[string]interface{}{
"order_id": order.ID,
},
err: err,
}
}
return nil
}
Best Practices
1. Error Context
Provide meaningful error context:
// Bad: insufficient context
return errors.New("validation failed")
// Good: rich context
return fmt.Errorf("order %s validation failed: invalid status %s",
order.ID, order.Status)
// Better: structured error
return &ValidationError{
Field: "status",
OrderID: order.ID,
Value: order.Status,
Message: "invalid order status",
}
2. Error Handling Patterns
Common patterns for different scenarios:
// Sentinel errors
var (
ErrNotFound = errors.New("not found")
ErrInvalid = errors.New("invalid input")
)
// Error types
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s with ID %s not found", e.Resource, e.ID)
}
// Error handling middleware
func ErrorHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
3. Error Logging
Proper error logging practices:
// Bad: logging and returning
if err != nil {
log.Printf("failed to process: %v", err)
return err
}
// Good: log at the top level
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
// Return errors without logging
if err := process(); err != nil {
return fmt.Errorf("failed to run: %w", err)
}
return nil
}
Common Patterns
1. Retry Pattern
Handle transient errors:
func WithRetry(fn func() error, attempts int, delay time.Duration) error {
var err error
for i := 0; i < attempts; i++ {
err = fn()
if err == nil {
return nil
}
if i < attempts-1 {
time.Sleep(delay)
}
}
return fmt.Errorf("failed after %d attempts: %w", attempts, err)
}
// Usage
err := WithRetry(func() error {
return callExternalService()
}, 3, time.Second)
2. Circuit Breaker
Prevent cascading failures:
type CircuitBreaker struct {
failures int64
threshold int64
resetAfter time.Duration
lastFailure time.Time
}
func (cb *CircuitBreaker) Execute(fn func() error) error {
if cb.isOpen() {
return errors.New("circuit breaker is open")
}
err := fn()
if err != nil {
cb.recordFailure()
return err
}
cb.reset()
return nil
}
Error Handling in Tests
1. Testing Error Conditions
func TestProcess(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
errType error
}{
{
name: "valid input",
input: "valid",
wantErr: false,
},
{
name: "invalid input",
input: "",
wantErr: true,
errType: ErrInvalid,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Process(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Process() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr && !errors.Is(err, tt.errType) {
t.Errorf("Process() error = %v, want %v", err, tt.errType)
}
})
}
}
Next Steps
- Learn about Logging
- Explore Configuration Management
- Study Code Style