d95c62bad4
Remove 557-line server-design.md from _go-learning/architecture - content is now covered in updated architecture documentation with real implementation examples and test coverage.
13 KiB
13 KiB
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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
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
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
// 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
// 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
// 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
// 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
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
// 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
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