# Error Wrapping Pattern in Go ## Pattern Overview Error Wrapping adds context to errors as they propagate up the call stack, creating a chain of errors that preserves both the original error and contextual information. Go 1.13+ provides `fmt.Errorf` with `%w` verb and `errors.Unwrap` for this pattern. ## Pattern Structure ```go // Wrap error with context if err != nil { return fmt.Errorf("operation failed: %w", err) } // Unwrap to get original error originalErr := errors.Unwrap(wrappedErr) // Check if error chain contains specific error if errors.Is(err, ErrNotFound) { // Handle not found } // Extract error of specific type from chain var domainErr *DomainError if errors.As(err, &domainErr) { // Use domain error } ``` ## Real Implementation from Project ### Basic Error Wrapping ```go // internal/models/cv/cv.go func LoadCV(lang string) (*CV, error) { // Build file path filePath := fmt.Sprintf("data/cv-%s.json", lang) // Read file data, err := os.ReadFile(filePath) if err != nil { // Wrap with context return nil, fmt.Errorf("failed to read CV file: %w", err) } // Parse JSON var cv CV err = json.Unmarshal(data, &cv) if err != nil { // Wrap with more context return nil, fmt.Errorf("failed to parse CV JSON: %w", err) } // Validate if err := cv.Validate(); err != nil { // Wrap validation error return nil, fmt.Errorf("CV validation failed: %w", err) } return &cv, nil } ``` ### Domain Error Type ```go // internal/handlers/errors.go // DomainError represents application-level errors type DomainError struct { Code ErrorCode // Machine-readable error code Message string // Human-readable message Err error // Underlying error StatusCode int // HTTP status code Field string // Field that caused error } // Error implements error interface func (e *DomainError) Error() string { if e.Err != nil { return fmt.Sprintf("%s: %v", e.Message, e.Err) } return e.Message } // Unwrap returns the underlying error func (e *DomainError) Unwrap() error { return e.Err } // WithErr adds underlying error func (e *DomainError) WithErr(err error) *DomainError { e.Err = err return e } // WithField adds field information func (e *DomainError) WithField(field string) *DomainError { e.Field = field return e } ``` ### Error Constructors ```go // internal/handlers/errors.go // InvalidLanguageError creates a language validation error func InvalidLanguageError(lang string) *DomainError { return NewDomainError( ErrCodeInvalidLanguage, fmt.Sprintf("Unsupported language: %s (use 'en' or 'es')", lang), http.StatusBadRequest, ).WithField("lang") } // DataNotFoundError creates a data not found error func DataNotFoundError(dataType, lang string) *DomainError { return NewDomainError( ErrCodeDataNotFound, fmt.Sprintf("%s data not found for language: %s", dataType, lang), http.StatusInternalServerError, ) } // PDFGenerationError creates a PDF generation error func PDFGenerationError(err error) *DomainError { return NewDomainError( ErrCodePDFGeneration, "Failed to generate PDF. Please try again.", http.StatusInternalServerError, ).WithErr(err) } ``` ### Error Handling Chain ```go // internal/handlers/cv_pages.go func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) { // Validate language if err := validateLanguage(lang); err != nil { // err is already a DomainError h.HandleError(w, r, err) return } // Load CV data cv, err := cvmodel.LoadCV(lang) if err != nil { // Wrap in DomainError with context domErr := DataNotFoundError("CV", lang).WithErr(err) h.HandleError(w, r, domErr) return } // Render template if err := h.tmpl.Render(w, "index.html", data); err != nil { // Wrap template error domErr := TemplateError(err) h.HandleError(w, r, domErr) return } } ``` ### Centralized Error Handler ```go // internal/handlers/errors.go // HandleError processes errors and sends appropriate HTTP response func (h *CVHandler) HandleError(w http.ResponseWriter, r *http.Request, err error) { // Try to cast to DomainError var domErr *DomainError if errors.As(err, &domErr) { // Handle domain error h.handleDomainError(w, r, domErr) return } // Check for specific errors if errors.Is(err, context.Canceled) { // Client disconnected log.Printf("Request canceled: %v", err) return } if errors.Is(err, context.DeadlineExceeded) { // Timeout domErr := NewDomainError( ErrCodeTimeout, "Request timed out", http.StatusGatewayTimeout, ) h.handleDomainError(w, r, domErr) return } // Generic error log.Printf("Unhandled error: %v", err) domErr := NewDomainError( ErrCodeInternalError, "An unexpected error occurred", http.StatusInternalServerError, ) h.handleDomainError(w, r, domErr) } func (h *CVHandler) handleDomainError(w http.ResponseWriter, r *http.Request, domErr *DomainError) { // Log error with code log.Printf("[ERROR] %s: %s", domErr.Code, domErr.Message) if domErr.Err != nil { log.Printf("[ERROR] Underlying: %v", domErr.Err) } // Build error response response := NewErrorResponse( string(domErr.Code), domErr.Message, ) if domErr.Field != "" { response.Error.Field = domErr.Field } // Send JSON response w.Header().Set("Content-Type", "application/json") w.WriteHeader(domErr.StatusCode) json.NewEncoder(w).Encode(response) } ``` ## Error Chain Example ### Full Error Propagation ``` 1. File system error (os.ReadFile) ↓ 2. Wrapped by model (LoadCV) "failed to read CV file: open data/cv-xx.json: no such file" ↓ 3. Wrapped by handler (Home) DataNotFoundError("CV", "xx").WithErr(err) ↓ 4. Handled by error handler { "success": false, "error": { "code": "DATA_NOT_FOUND", "message": "CV data not found for language: xx" } } ``` ### Error Chain in Code ```go // Layer 1: File system _, err := os.ReadFile("data/cv-xx.json") // err = &fs.PathError{Op:"open", Path:"data/cv-xx.json", Err:syscall.ENOENT} // Layer 2: Model if err != nil { return nil, fmt.Errorf("failed to read CV file: %w", err) } // err = "failed to read CV file: open data/cv-xx.json: no such file or directory" // Layer 3: Handler if err != nil { domErr := DataNotFoundError("CV", lang).WithErr(err) } // domErr = &DomainError{ // Code: "DATA_NOT_FOUND", // Message: "CV data not found for language: xx", // Err: [wrapped error from model], // StatusCode: 500, // } // Layer 4: Error handler h.HandleError(w, r, domErr) // Logs full chain, sends user-friendly JSON ``` ## Using errors.Is and errors.As ### errors.Is - Check Error Type ```go func handleError(err error) { // Check if error is or wraps specific error if errors.Is(err, os.ErrNotExist) { fmt.Println("File not found") return } if errors.Is(err, context.Canceled) { fmt.Println("Request canceled") return } if errors.Is(err, sql.ErrNoRows) { fmt.Println("No data found") return } fmt.Println("Unknown error:", err) } ``` ### errors.As - Extract Error Type ```go func handleError(err error) { // Extract DomainError from chain var domErr *DomainError if errors.As(err, &domErr) { fmt.Printf("Domain error: code=%s, status=%d\n", domErr.Code, domErr.StatusCode) return } // Extract PathError from chain var pathErr *fs.PathError if errors.As(err, &pathErr) { fmt.Printf("Path error: op=%s, path=%s\n", pathErr.Op, pathErr.Path) return } fmt.Println("Unknown error:", err) } ``` ## Custom Error Types ### Sentinel Errors ```go // Define sentinel errors for comparison var ( ErrNotFound = errors.New("not found") ErrUnauthorized = errors.New("unauthorized") ErrInvalidInput = errors.New("invalid input") ) // Use in code if user == nil { return ErrNotFound } // Check with errors.Is if errors.Is(err, ErrNotFound) { // Handle not found } ``` ### Error with Context ```go // ValidationError includes field information type ValidationError struct { Field string Message string Value interface{} } func (e *ValidationError) Error() string { return fmt.Sprintf("validation failed for %s: %s (value: %v)", e.Field, e.Message, e.Value) } // Usage if len(name) == 0 { return &ValidationError{ Field: "name", Message: "name is required", Value: name, } } ``` ## Error Wrapping Best Practices ### ✅ DO ```go // Add context when wrapping if err != nil { return fmt.Errorf("failed to process user %d: %w", userID, err) } // Use %w for wrapping (preserves error chain) return fmt.Errorf("database query failed: %w", err) // Wrap at each layer func LoadUser(id int) (*User, error) { data, err := readFile(fmt.Sprintf("users/%d.json", id)) if err != nil { return nil, fmt.Errorf("load user %d: %w", id, err) } // ... } // Create custom errors with context func InvalidEmailError(email string) error { return fmt.Errorf("invalid email format: %s", email) } // Check errors with errors.Is if errors.Is(err, os.ErrNotExist) { // Handle file not found } // Extract errors with errors.As var domErr *DomainError if errors.As(err, &domErr) { // Use domain error } ``` ### ❌ DON'T ```go // DON'T use %v (loses error chain) return fmt.Errorf("failed: %v", err) // Wrong! return fmt.Errorf("failed: %w", err) // Correct // DON'T ignore errors data, _ := readFile(path) // Wrong! // DON'T return generic errors if invalid { return errors.New("error") // Too generic! } // DON'T compare errors with == if err == someError { // Wrong! Use errors.Is // ... } // DON'T type assert directly domErr := err.(*DomainError) // Wrong! Use errors.As ``` ## Error Logging ### Structured Logging ```go func (h *Handler) processRequest(r *http.Request) error { err := h.doWork() if err != nil { // Log with context log.Printf("[ERROR] Request processing failed: %v", err) // Log underlying errors var domErr *DomainError if errors.As(err, &domErr) { log.Printf("[ERROR] Code: %s, Status: %d", domErr.Code, domErr.StatusCode) if domErr.Err != nil { log.Printf("[ERROR] Underlying: %v", domErr.Err) } } return err } return nil } ``` ### Stack Traces ```go // For panics (recovered in middleware) func Recovery(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { // Log with stack trace log.Printf("PANIC: %v\n%s", err, debug.Stack()) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } ``` ## Testing Error Handling ```go func TestLoadCV_FileNotFound(t *testing.T) { // Test error wrapping _, err := LoadCV("nonexistent") // Check error occurred if err == nil { t.Fatal("expected error, got nil") } // Check error message contains context if !strings.Contains(err.Error(), "failed to read CV file") { t.Errorf("error missing context: %v", err) } // Check error chain contains specific error if !errors.Is(err, os.ErrNotExist) { t.Error("error should wrap os.ErrNotExist") } } func TestHandleError_DomainError(t *testing.T) { // Create domain error domErr := InvalidLanguageError("xx") // Test handling w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) handler.HandleError(w, req, domErr) // Verify response if w.Code != http.StatusBadRequest { t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest) } var response APIResponse json.NewDecoder(w.Body).Decode(&response) if response.Success { t.Error("expected success=false") } if response.Error.Code != "INVALID_LANGUAGE" { t.Errorf("code = %s, want INVALID_LANGUAGE", response.Error.Code) } } ``` ## Related Patterns - **Handler Pattern**: Uses error wrapping for error handling - **Context Pattern**: context.Canceled and context.DeadlineExceeded errors - **Factory Pattern**: Error constructors create wrapped errors ## Further Reading - [Go Error Handling](https://go.dev/blog/error-handling-and-go) - [Working with Errors](https://go.dev/blog/go1.13-errors) - [Error Handling Best Practices](https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully)