Files
cv-site/doc/_go-learning/diagrams/06-error-handling-flow.md
T
juanatsap d95c62bad4 refactor: remove outdated server design documentation
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.
2025-12-02 20:25:05 +00:00

29 KiB

Error Handling Flow Diagram

Error Handling Architecture

┌──────────────────────────────────────────────────────────────┐
│                 Error Handling Architecture                   │
└──────────────────────────────────────────────────────────────┘

Error Types:
├── Domain Errors        Application-level business logic errors
├── Validation Errors    Input validation failures
├── System Errors        Infrastructure/system failures
└── Panic Recovery       Runtime panic handling

Domain Error Structure

┌──────────────────────────────────────────────────────────────┐
│              DomainError (internal/handlers/errors.go)        │
├──────────────────────────────────────────────────────────────┤
│  type DomainError struct {                                    │
│      Code       ErrorCode  // Enum error code                 │
│      Message    string     // Human-readable message          │
│      Err        error      // Underlying error (if any)       │
│      StatusCode int        // HTTP status code                │
│      Field      string     // Field that caused error         │
│  }                                                            │
│                                                               │
│  func (e *DomainError) Error() string                         │
│  func (e *DomainError) Unwrap() error                         │
│  func (e *DomainError) WithField(field string) *DomainError   │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│                      Error Codes                              │
├──────────────────────────────────────────────────────────────┤
│  type ErrorCode string                                        │
│                                                               │
│  const (                                                      │
│      // Input Validation (400)                               │
│      ErrCodeInvalidLanguage   = "INVALID_LANGUAGE"           │
│      ErrCodeInvalidLength     = "INVALID_LENGTH"             │
│      ErrCodeInvalidIcons      = "INVALID_ICONS"              │
│      ErrCodeInvalidTheme      = "INVALID_THEME"              │
│      ErrCodeInvalidVersion    = "INVALID_VERSION"            │
│      ErrCodeValidationFailed  = "VALIDATION_FAILED"          │
│                                                               │
│      // Resource Errors (404, 500)                           │
│      ErrCodeDataNotFound      = "DATA_NOT_FOUND"             │
│      ErrCodeTemplateNotFound  = "TEMPLATE_NOT_FOUND"         │
│      ErrCodeTemplateError     = "TEMPLATE_ERROR"             │
│                                                               │
│      // Processing Errors (500)                              │
│      ErrCodePDFGeneration     = "PDF_GENERATION"             │
│      ErrCodeInternalError     = "INTERNAL_ERROR"             │
│                                                               │
│      // Rate Limiting (429)                                  │
│      ErrCodeRateLimitExceeded = "RATE_LIMIT_EXCEEDED"        │
│                                                               │
│      // Security (403)                                       │
│      ErrCodeOriginMismatch    = "ORIGIN_MISMATCH"            │
│  )                                                            │
└──────────────────────────────────────────────────────────────┘

Error Flow Patterns

Pattern 1: Validation Error

┌────────────────────────────────────────────────────────────┐
│                  Validation Error Flow                      │
└────────────────────────────────────────────────────────────┘

Request: GET /?lang=xx
     │
     ▼
┌─────────────────────────┐
│  Handler.Home()         │
│  (cv_pages.go)          │
└─────────────────────────┘
     │
     ├─→ lang := r.URL.Query().Get("lang")
     │   // lang = "xx"
     │
     ├─→ err := validateLanguage(lang)
     │   // "xx" not in ["en", "es"]
     │
     ▼
┌─────────────────────────────────────────────────────────────┐
│  validateLanguage(lang)                                      │
│  (cv_helpers.go)                                            │
│                                                              │
│  if lang != "en" && lang != "es" {                          │
│      return InvalidLanguageError(lang)                      │
│  }                                                           │
└─────────────────────────────────────────────────────────────┘
     │
     ▼
┌─────────────────────────────────────────────────────────────┐
│  InvalidLanguageError(lang)                                  │
│  (errors.go)                                                │
│                                                              │
│  return NewDomainError(                                      │
│      ErrCodeInvalidLanguage,                                │
│      fmt.Sprintf("Unsupported language: %s", lang),         │
│      http.StatusBadRequest,                                 │
│  ).WithField("lang")                                        │
└─────────────────────────────────────────────────────────────┘
     │
     ▼
┌─────────────────────────────────────────────────────────────┐
│  Handler receives error                                      │
│                                                              │
│  if err != nil {                                             │
│      h.HandleError(w, r, err)                               │
│      return                                                  │
│  }                                                           │
└─────────────────────────────────────────────────────────────┘
     │
     ▼
┌─────────────────────────────────────────────────────────────┐
│  HandleError(w, r, err)                                      │
│  (errors.go)                                                │
│                                                              │
│  1. Cast to DomainError                                      │
│     domErr, ok := err.(*DomainError)                        │
│                                                              │
│  2. Log error                                                │
│     log.Printf("[ERROR] %s: %s", domErr.Code, domErr.Message) │
│                                                              │
│  3. Build response                                           │
│     response := NewErrorResponse(                            │
│         string(domErr.Code),                                │
│         domErr.Message,                                     │
│     )                                                        │
│     response.Error.Field = domErr.Field                     │
│                                                              │
│  4. Send JSON error                                          │
│     w.Header().Set("Content-Type", "application/json")      │
│     w.WriteHeader(domErr.StatusCode)                        │
│     json.NewEncoder(w).Encode(response)                     │
└─────────────────────────────────────────────────────────────┘
     │
     ▼
┌─────────────────────────────────────────────────────────────┐
│  Client Response                                             │
│                                                              │
│  HTTP/1.1 400 Bad Request                                   │
│  Content-Type: application/json                             │
│                                                              │
│  {                                                           │
│    "success": false,                                        │
│    "error": {                                               │
│      "code": "INVALID_LANGUAGE",                           │
│      "message": "Unsupported language: xx (use 'en' or 'es')", │
│      "field": "lang"                                        │
│    }                                                         │
│  }                                                           │
└─────────────────────────────────────────────────────────────┘

Pattern 2: Data Loading Error

┌────────────────────────────────────────────────────────────┐
│                  Data Loading Error Flow                    │
└────────────────────────────────────────────────────────────┘

Handler calls LoadCV("es")
     │
     ▼
┌─────────────────────────────────────────────────────────────┐
│  cvmodel.LoadCV(lang)                                        │
│  (internal/models/cv/cv.go)                                 │
│                                                              │
│  1. Build file path                                          │
│     filePath := fmt.Sprintf("data/cv-%s.json", lang)        │
│                                                              │
│  2. Read file                                                │
│     data, err := os.ReadFile(filePath)                      │
│     if err != nil {                                          │
│         return nil, fmt.Errorf("failed to read CV: %w", err) │
│     }                                                        │
│                                                              │
│  3. Parse JSON                                               │
│     var cv CV                                                │
│     err = json.Unmarshal(data, &cv)                         │
│     if err != nil {                                          │
│         return nil, fmt.Errorf("failed to parse CV: %w", err) │
│     }                                                        │
│                                                              │
│  4. Validate                                                 │
│     if err := cv.Validate(); err != nil {                   │
│         return nil, fmt.Errorf("invalid CV data: %w", err)  │
│     }                                                        │
│                                                              │
│  5. Return                                                   │
│     return &cv, nil                                         │
└─────────────────────────────────────────────────────────────┘
     │
     │ Error Case: File not found
     ▼
┌─────────────────────────────────────────────────────────────┐
│  Handler receives error                                      │
│                                                              │
│  cv, err := cvmodel.LoadCV(lang)                            │
│  if err != nil {                                             │
│      // Wrap in DomainError                                 │
│      domErr := DataNotFoundError("CV", lang)                │
│      domErr.Err = err                                       │
│      h.HandleError(w, r, domErr)                            │
│      return                                                  │
│  }                                                           │
└─────────────────────────────────────────────────────────────┘
     │
     ▼
┌─────────────────────────────────────────────────────────────┐
│  Client Response                                             │
│                                                              │
│  HTTP/1.1 500 Internal Server Error                         │
│  Content-Type: application/json                             │
│                                                              │
│  {                                                           │
│    "success": false,                                        │
│    "error": {                                               │
│      "code": "DATA_NOT_FOUND",                             │
│      "message": "CV data not found for language: es"        │
│    }                                                         │
│  }                                                           │
└─────────────────────────────────────────────────────────────┘

Pattern 3: PDF Generation Error

┌────────────────────────────────────────────────────────────┐
│                PDF Generation Error Flow                    │
└────────────────────────────────────────────────────────────┘

Handler calls GeneratePDF()
     │
     ▼
┌─────────────────────────────────────────────────────────────┐
│  pdf.GeneratePDF(htmlContent, options)                      │
│  (internal/pdf/generator.go)                                │
│                                                              │
│  1. Create context                                           │
│     ctx, cancel := chromedp.NewContext(...)                 │
│     defer cancel()                                          │
│                                                              │
│  2. Launch Chrome                                            │
│     if err := chromedp.Run(ctx, ...); err != nil {          │
│         return nil, fmt.Errorf("chrome launch: %w", err)    │
│     }                                                        │
│                                                              │
│  3. Navigate and render                                      │
│     err := chromedp.Run(ctx,                                │
│         chromedp.Navigate(dataURL),                         │
│         chromedp.WaitReady("body"),                         │
│         chromedp.PrintToPDF(&pdfBytes),                     │
│     )                                                        │
│     if err != nil {                                          │
│         return nil, fmt.Errorf("PDF generation: %w", err)   │
│     }                                                        │
│                                                              │
│  4. Return PDF bytes                                         │
│     return pdfBytes, nil                                    │
└─────────────────────────────────────────────────────────────┘
     │
     │ Error Case: Chrome failed
     ▼
┌─────────────────────────────────────────────────────────────┐
│  Handler.ExportPDF receives error                            │
│  (cv_pdf.go)                                                │
│                                                              │
│  pdfBytes, err := pdf.GeneratePDF(htmlContent, opts)        │
│  if err != nil {                                             │
│      domErr := PDFGenerationError(err)                      │
│      h.HandleError(w, r, domErr)                            │
│      return                                                  │
│  }                                                           │
└─────────────────────────────────────────────────────────────┘
     │
     ▼
┌─────────────────────────────────────────────────────────────┐
│  Client Response                                             │
│                                                              │
│  HTTP/1.1 500 Internal Server Error                         │
│  Content-Type: application/json                             │
│                                                              │
│  {                                                           │
│    "success": false,                                        │
│    "error": {                                               │
│      "code": "PDF_GENERATION",                             │
│      "message": "Failed to generate PDF. Please try again." │
│    }                                                         │
│  }                                                           │
└─────────────────────────────────────────────────────────────┘

Pattern 4: Panic Recovery

┌────────────────────────────────────────────────────────────┐
│                    Panic Recovery Flow                      │
└────────────────────────────────────────────────────────────┘

Request enters system
     │
     ▼
┌─────────────────────────────────────────────────────────────┐
│  Recovery Middleware                                         │
│  (internal/middleware/recovery.go)                          │
│                                                              │
│  func Recovery(next http.Handler) http.Handler {            │
│      return http.HandlerFunc(func(w, r) {                   │
│          defer func() {                                     │
│              if err := recover(); err != nil {              │
│                  // Capture panic                           │
│                  stack := debug.Stack()                     │
│                                                              │
│                  // Log with stack trace                    │
│                  log.Printf("PANIC: %v\n%s", err, stack)    │
│                                                              │
│                  // Send error response                     │
│                  http.Error(w,                              │
│                      "Internal Server Error",               │
│                      http.StatusInternalServerError)        │
│              }                                               │
│          }()                                                 │
│                                                              │
│          // Continue to next handler                        │
│          next.ServeHTTP(w, r)                               │
│      })                                                      │
│  }                                                           │
└─────────────────────────────────────────────────────────────┘
     │
     │ Normal flow: no panic
     ├──────────────────────────────────┐
     ▼                                  │ Panic occurs
Handler executes                        ▼
     │                    ┌─────────────────────────────────┐
     │                    │  panic("something went wrong")  │
     │                    └─────────────────────────────────┘
     │                                  │
     │                                  ▼
     │                    ┌─────────────────────────────────┐
     │                    │  defer recover() catches it     │
     │                    │  ├─ Get stack trace             │
     │                    │  ├─ Log error + stack           │
     │                    │  └─ Send 500 response           │
     │                    └─────────────────────────────────┘
     ▼                                  │
Response sent                           ▼
                         ┌─────────────────────────────────┐
                         │  Client receives 500            │
                         └─────────────────────────────────┘

Error Response Formats

┌────────────────────────────────────────────────────────────┐
│                 Error Response Formats                      │
└────────────────────────────────────────────────────────────┘

Standard API Error (JSON):
{
  "success": false,
  "error": {
    "code": "INVALID_LANGUAGE",
    "message": "Unsupported language: xx (use 'en' or 'es')",
    "field": "lang"
  }
}

Validation Error with Multiple Fields:
{
  "success": false,
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Request validation failed",
    "fields": {
      "lang": "Invalid language",
      "length": "Invalid length"
    }
  }
}

Internal Error (Generic):
{
  "success": false,
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "An unexpected error occurred. Please try again."
  }
}

HTML Error Page (for page requests):
<!DOCTYPE html>
<html>
<head><title>Error</title></head>
<body>
  <h1>Oops! Something went wrong</h1>
  <p>We're sorry, but we couldn't process your request.</p>
  <p>Error: INVALID_LANGUAGE</p>
  <a href="/">Go back home</a>
</body>
</html>

Error Logging

┌────────────────────────────────────────────────────────────┐
│                     Error Logging                           │
└────────────────────────────────────────────────────────────┘

Log Format:
[ERROR] <ERROR_CODE>: <message>
[ERROR] Additional context: <details>
[ERROR] Stack trace (if panic):
<stack trace lines>

Examples:

[ERROR] INVALID_LANGUAGE: Unsupported language: xx (use 'en' or 'es')
[ERROR] Field: lang

[ERROR] PDF_GENERATION: Failed to generate PDF
[ERROR] Underlying error: chrome launch failed: context deadline exceeded

[ERROR] PANIC: runtime error: invalid memory address
[ERROR] Stack trace:
goroutine 23 [running]:
main.(*CVHandler).Home(...)
    /app/internal/handlers/cv_pages.go:42
...

Error Handling Best Practices

┌────────────────────────────────────────────────────────────┐
│              Error Handling Best Practices                  │
└────────────────────────────────────────────────────────────┘

1. USE TYPED ERRORS
   ✓ return InvalidLanguageError(lang)
   ✗ return errors.New("invalid language")

2. WRAP ERRORS WITH CONTEXT
   ✓ return fmt.Errorf("failed to load CV: %w", err)
   ✗ return err

3. LOG BEFORE RESPONDING
   ✓ log.Printf("[ERROR] %s", err)
      h.HandleError(w, r, err)
   ✗ h.HandleError(w, r, err) // No logging

4. USE APPROPRIATE STATUS CODES
   ✓ 400 for validation errors
      404 for not found
      429 for rate limiting
      500 for server errors
   ✗ Always returning 500

5. DON'T LEAK INTERNAL DETAILS
   ✓ "Failed to generate PDF. Please try again."
   ✗ "chromedp: chrome crashed at line 42 in generator.go"

6. PROVIDE ACTIONABLE MESSAGES
   ✓ "Unsupported language: xx (use 'en' or 'es')"
   ✗ "Invalid input"

7. USE RECOVERY MIDDLEWARE
   ✓ Catch all panics at middleware level
   ✗ Let panics crash the server

8. INCLUDE FIELD INFORMATION
   ✓ error.WithField("lang")
   ✗ Generic error without field context