Modern Error Handling with Wrapping
Models validation, database, and filesystem failures as rich errors and shows how to unwrap or classify them safely.
main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
// Custom error types for domain-specific errors
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
}
type DatabaseError struct {
Operation string
Err error
}
func (e *DatabaseError) Error() string {
return fmt.Sprintf("database error during %s: %v", e.Operation, e.Err)
}
// Unwrap implements error unwrapping for DatabaseError
func (e *DatabaseError) Unwrap() error {
return e.Err
}
// UserService handles user operations with proper error handling
type UserService struct{}
// ValidateUser checks if user data is valid
func (s *UserService) ValidateUser(username, email string) error {
if username == "" {
return &ValidationError{
Field: "username",
Message: "cannot be empty",
}
}
if email == "" {
return &ValidationError{
Field: "email",
Message: "cannot be empty",
}
}
if len(username) < 3 {
return &ValidationError{
Field: "username",
Message: "must be at least 3 characters long",
}
}
return nil
}
// SaveUser simulates saving a user to a database
func (s *UserService) SaveUser(username, email string) error {
// First validate user
if err := s.ValidateUser(username, email); err != nil {
// Wrap validation error with context
return fmt.Errorf("failed to validate user before save: %w", err)
}
// Simulate database error
dbErr := errors.New("connection timeout")
return &DatabaseError{
Operation: "insert",
Err: dbErr,
}
}
// FileOperation simulates a file operation with wrapped errors
func FileOperation(filename string) error {
file, err := os.Open(filename)
if err != nil {
// Wrap the system error with custom context
return fmt.Errorf("failed to open file '%s': %w", filename, err)
}
defer file.Close()
// Do something with the file...
return nil
}
func main() {
// Example 1: Custom error types with wrapping
userService := &UserService{}
err := userService.SaveUser("jo", "[email protected]")
if err != nil {
fmt.Println("Error saving user:", err)
// Check if error is a ValidationError (using errors.As)
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Validation failed for field '%s': %s\n", valErr.Field, valErr.Message)
}
// Check if error is a DatabaseError
var dbErr *DatabaseError
if errors.As(err, &dbErr) {
fmt.Printf("Database error during %s: %v\n", dbErr.Operation, dbErr.Err)
}
// Unwrap and check the original error
rootErr := errors.Unwrap(err)
fmt.Printf("Root error: %v\n", rootErr)
}
fmt.Println("---")
// Example 2: Wrapping standard library errors
err = FileOperation("nonexistent.txt")
if err != nil {
fmt.Println("File operation error:", err)
// Check if the wrapped error is fs.ErrNotExist
if errors.Is(err, fs.ErrNotExist) {
fmt.Println("The file does not exist (detected via errors.Is)")
}
// Extract the original error
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
fmt.Printf("Path error details: Op=%s, Path=%s, Err=%v\n", pathErr.Op, pathErr.Path, pathErr.Err)
}
}
// Example 3: Chained error wrapping
originalErr := errors.New("original error")
wrappedErr1 := fmt.Errorf("level 1 wrap: %w", originalErr)
wrappedErr2 := fmt.Errorf("level 2 wrap: %w", wrappedErr1)
fmt.Println("---")
fmt.Println("Chained errors:")
fmt.Println("Wrapped 2:", wrappedErr2)
fmt.Println("Is original error?", errors.Is(wrappedErr2, originalErr))
fmt.Println("Unwrap once:", errors.Unwrap(wrappedErr2))
fmt.Println("Unwrap twice:", errors.Unwrap(errors.Unwrap(wrappedErr2)))
}How It Works
Models validation, database, and filesystem failures as rich errors and shows how to unwrap or classify them safely.
Defines ValidationError and DatabaseError types that implement Error and Unwrap; helper functions return wrapped errors with context; main exercises validation, database operation, file read, and chained wrapping, then uses errors.Is and errors.As to branch on causes or compare to fs.ErrNotExist.
Key Concepts
- 1Custom error types carry structured context such as field or operation.
- 2Wrapping with %w preserves the cause for Is and As checks.
- 3Sentinel errors like fs.ErrNotExist are surfaced for callers to branch on.
- 4Demonstrates error chains and unwrapping depth for debugging.
When to Use This Pattern
- Mapping domain errors to HTTP status codes or gRPC codes.
- Logging root causes without losing higher-level context.
- Writing tests that assert specific error categories.
- Building libraries with predictable, typed error surfaces.
Best Practices
- Wrap errors at each layer with actionable context strings.
- Expose typed errors or sentinel variables for consumers to match.
- Avoid panics for expected error cases; return errors instead.
- When logging, include both the outer message and the unwrapped cause.
Go Version1.13+
Difficultyintermediate
Production ReadyYes
Lines of Code146