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