Documentation

Error Handling

Overview

The framework provides structured error handling patterns that ensure consistent error management across all application layers. Error handling is designed to be explicit, type-safe, and informative.

Error Types

Validation Errors

Validation errors occur when input data doesn’t meet business rules or format requirements:

type ValidationError struct {
    Field   string
    Message string
    Value   interface{}
}

Common scenarios: - Required fields missing - Invalid data formats - Business rule violations - Field length constraints

Database Errors

Database operations can fail for various reasons:

type DatabaseError struct {
    Operation string
    Table     string
    Err       error
}

Common scenarios: - Connection failures - Constraint violations - Record not found - Transaction rollbacks

Business Logic Errors

Custom errors for specific business scenarios:

type BusinessError struct {
    Code    string
    Message string
    Context map[string]interface{}
}

Examples: - Insufficient permissions - Resource conflicts - State transition violations - Quota exceeded

Error Handling Layers

Service Layer

Services return structured errors that can be handled appropriately by calling layers:

Fetchers - Return nil, false, nil for not found (not an error) - Return nil, false, error for actual errors - Distinguish between “not found” and “error occurred”

Handlers - Wrap database errors with operation context - Return specific errors for constraint violations - Handle transaction rollbacks gracefully

Validators - Return detailed validation errors with field information - Support multiple validation errors - Provide actionable error messages

Maestro Layer

Maestros orchestrate error handling across multiple services:

func (this *CreateGardenMae) Execute(ctx context.Context, input *CreateGardenMaeInput) (*CreateGardenMaeOutput, error) {
    // Step 1: Validation
    if err := this.GardenValidator.ValidateCreateForm(input.Form); err != nil {
        return nil, fmt.Errorf("validation failed: %w", err)
    }

    // Step 2: Business logic
    if err := this.checkBusinessRules(ctx, input.Form); err != nil {
        return nil, fmt.Errorf("business rule violation: %w", err)
    }

    // Step 3: Persistence
    garden, err := this.createGarden(ctx, input.Form)
    if err != nil {
        return nil, fmt.Errorf("failed to create garden: %w", err)
    }

    return &CreateGardenMaeOutput{Garden: garden}, nil
}

Controller Layer

Controllers translate errors into appropriate HTTP responses:

Error Response Mapping: - 400 Bad Request - Validation errors - 404 Not Found - Resource not found - 409 Conflict - Business rule violations - 500 Internal Server Error - System errors

Error Response Patterns

Structured Error Responses

Controllers return consistent error response formats:

func (gc *GardenController) handleError(c *gin.Context, err error) {
    var validationErr *ValidationError
    var businessErr *BusinessError
    var dbErr *DatabaseError

    switch {
    case errors.As(err, &validationErr):
        c.JSON(400, gin.H{
            "error": "Validation failed",
            "field": validationErr.Field,
            "message": validationErr.Message,
            "code": "VALIDATION_ERROR",
        })

    case errors.As(err, &businessErr):
        c.JSON(409, gin.H{
            "error": businessErr.Message,
            "code": businessErr.Code,
            "context": businessErr.Context,
        })

    case errors.As(err, &dbErr):
        // Log detailed error, return generic message
        log.Printf("Database error: %v", dbErr)
        c.JSON(500, gin.H{
            "error": "Internal server error",
            "code": "INTERNAL_ERROR",
        })

    default:
        // Unknown error - log and return generic response
        log.Printf("Unknown error: %v", err)
        c.JSON(500, gin.H{
            "error": "Internal server error",
            "code": "UNKNOWN_ERROR",
        })
    }
}

API Error Format

Standard API error response structure:

{
  "error": "Human readable error message",
  "code": "MACHINE_READABLE_CODE",
  "field": "field_name",
  "details": {
    "additional": "context information"
  }
}

HTML Error Handling

For web pages, errors are displayed in user-friendly formats:

func (gc *GardenController) Create(c *gin.Context) {
    form := &forms.CreateGardenForm{}
    if err := c.ShouldBind(form); err != nil {
        c.HTML(400, "gardens/new.html", gin.H{
            "error": "Please check your input and try again",
            "form": form,
        })
        return
    }

    output, err := gc.CreateGardenMae.Execute(c.Request.Context(), &maes.CreateGardenMaeInput{
        Form: form,
    })

    if err != nil {
        c.HTML(400, "gardens/new.html", gin.H{
            "error": err.Error(),
            "form": form,
        })
        return
    }

    c.Redirect(302, "/gardens/" + output.Garden.Id)
}

Error Context and Wrapping

Error Wrapping

Use error wrapping to maintain error chains while adding context:

// In services
func (h *GardenHandler) Create(ctx context.Context, garden *mdl.Garden, mod *handler.HandlerMod) (*mdl.Garden, error) {
    createdGarden, err := h.baseHandler.Create(ctx, garden, mod)
    if err != nil {
        return nil, fmt.Errorf("failed to create garden '%s': %w", garden.Name, err)
    }
    return createdGarden, nil
}

// In Maestros
func (m *CreateGardenMae) Execute(ctx context.Context, input *CreateGardenMaeInput) (*CreateGardenMaeOutput, error) {
    garden, err := m.GardenHandler.Create(ctx, gardenEntity, nil)
    if err != nil {
        return nil, fmt.Errorf("garden creation failed for user %s: %w", input.UserId, err)
    }
    // ...
}

Context Information

Include relevant context in errors:

type ContextualError struct {
    Operation string
    EntityId  string
    UserId    string
    Timestamp time.Time
    Err       error
}

func (e *ContextualError) Error() string {
    return fmt.Sprintf("operation '%s' failed for entity %s (user: %s) at %v: %v",
        e.Operation, e.EntityId, e.UserId, e.Timestamp, e.Err)
}

Logging and Monitoring

Error Logging

Log errors with appropriate detail levels:

func (gc *GardenController) handleError(c *gin.Context, err error) {
    requestID := c.GetString("request_id")
    userID := c.GetString("user_id")

    // Log with context
    log.WithFields(log.Fields{
        "request_id": requestID,
        "user_id": userID,
        "path": c.Request.URL.Path,
        "error": err.Error(),
    }).Error("Request failed")

    // Return appropriate response...
}

Error Monitoring

Track error patterns for system health:

type ErrorTracker struct {
    errorCounts map[string]int
    mutex       sync.RWMutex
}

func (et *ErrorTracker) RecordError(errorType string) {
    et.mutex.Lock()
    defer et.mutex.Unlock()
    et.errorCounts[errorType]++
}

func (et *ErrorTracker) GetErrorStats() map[string]int {
    et.mutex.RLock()
    defer et.mutex.RUnlock()

    stats := make(map[string]int)
    for k, v := range et.errorCounts {
        stats[k] = v
    }
    return stats
}

Recovery and Resilience

Panic Recovery

Handle panics gracefully in middleware:

func RecoveryMiddleware() gin.HandlerFunc {
    return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
        if err, ok := recovered.(string); ok {
            log.Printf("Panic recovered: %s", err)
        }

        c.JSON(500, gin.H{
            "error": "Internal server error",
            "code": "PANIC_RECOVERED",
        })
    })
}

Transaction Rollback

Ensure database consistency during errors:

func (m *ComplexGardenMae) Execute(ctx context.Context, input *ComplexGardenMaeInput) (*ComplexGardenMaeOutput, error) {
    tx, err := m.SqlDb.Begin(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed to begin transaction: %w", err)
    }
    defer tx.Rollback() // Rollback if not committed

    handlerMod := &handler.HandlerMod{Tx: tx}

    // Perform operations...
    garden, err := m.GardenHandler.Create(ctx, gardenEntity, handlerMod)
    if err != nil {
        return nil, fmt.Errorf("failed to create garden: %w", err)
    }

    // Commit transaction
    if err := tx.Commit(); err != nil {
        return nil, fmt.Errorf("failed to commit transaction: %w", err)
    }

    return &ComplexGardenMaeOutput{Garden: garden}, nil
}

Testing Error Scenarios

Unit Testing Errors

Test error conditions explicitly:

func TestCreateGardenMae_ValidationError(t *testing.T) {
    mae := setupCreateGardenMae()

    input := &maes.CreateGardenMaeInput{
        Form: &forms.CreateGardenForm{
            Name: "", // Invalid - empty name
        },
    }

    output, err := mae.Execute(context.Background(), input)

    assert.Error(t, err)
    assert.Nil(t, output)

    var validationErr *ValidationError
    assert.True(t, errors.As(err, &validationErr))
    assert.Equal(t, "name", validationErr.Field)
}

Integration Testing

Test complete error flows:

func TestGardenController_CreateValidationError(t *testing.T) {
    container := setupTestContainer()
    router := setupTestRouter(container)

    w := httptest.NewRecorder()
    req := httptest.NewRequest("POST", "/gardens", strings.NewReader(`{"name": ""}`))
    req.Header.Set("Content-Type", "application/json")

    router.ServeHTTP(w, req)

    assert.Equal(t, 400, w.Code)

    var response map[string]interface{}
    json.Unmarshal(w.Body.Bytes(), &response)
    assert.Equal(t, "VALIDATION_ERROR", response["code"])
}

Best Practices

Error Design Principles

  1. Be Explicit - Don’t hide errors or fail silently
  2. Be Specific - Provide actionable error information
  3. Be Consistent - Use standard error types and responses
  4. Be Secure - Don’t expose sensitive information in errors
  5. Be Helpful - Include context and suggestions when possible

Common Patterns

  • Fail Fast - Validate early and return errors immediately
  • Error Boundaries - Handle errors at appropriate layers
  • Graceful Degradation - Provide fallbacks when possible
  • User-Friendly Messages - Translate technical errors for end users
  • Comprehensive Logging - Log enough detail for debugging

Error handling is fundamental to building reliable applications. The framework’s structured approach ensures consistent, maintainable error management across all application layers.