# ErrorHandler Enhancements Guide

Documentation for ErrorHandler improvements that leverage the ErrorCode and ExceptionContext system.

## Overview

The ErrorHandler has been enhanced to automatically leverage ErrorCode metadata from FrameworkException, providing:
- **Automatic HTTP status mapping** based on error categories
- **Severity-based logging** with correct log levels
- **Recovery hints** in debug mode
- **Automatic Retry-After headers** for recoverable errors
- **Rich error metadata** in API responses

## Architecture

```
Exception thrown
    ↓
ErrorHandler.createExceptionMetadata()
    ↓
├── Extract ErrorCode metadata (if FrameworkException)
├── Determine HTTP status (ErrorCode-based → Legacy fallback)
├── Determine error level (ErrorCode.getSeverity() → Legacy fallback)
├── Add recovery hints (if debug mode)
├── Add Retry-After headers (if applicable)
    ↓
Response with enhanced metadata
```

## New Features

### 1. ErrorCode-Based HTTP Status Mapping

The ErrorHandler now uses ErrorCode categories for intelligent HTTP status determination.

**Priority System**:
1. **ErrorCode Category Mapping** (for FrameworkException)
2. **Legacy Exception Type Mapping** (fallback for non-FrameworkException)

**Category → HTTP Status Mapping**:
```php
match ($category) {
    'AUTH' => 401,    // Authentication errors
    'AUTHZ' => 403,   // Authorization errors
    'VAL' => 400,     // Validation errors
    'HTTP' => dynamic // Fine-grained HTTP error mapping
    'DB', 'QUEUE', 'CACHE', 'FILE' => 500, // Infrastructure errors
    default => 500,
};
```

**Example**:
```php
// Exception with AUTH category
throw InvalidCredentialsException::forUser($email);
// → Automatic HTTP 401 Unauthorized

// Exception with VAL category
throw ValidationException::forField($field, $errors);
// → Automatic HTTP 400 Bad Request

// Exception with QUEUE category
throw JobNotFoundException::byId($jobId);
// → Automatic HTTP 500 Internal Server Error
```

### 2. Severity-Based Error Level Mapping

ErrorCode severity levels are now mapped to ErrorHandler ErrorLevels for correct logging.

**Mapping**:
```php
ErrorCode.getSeverity() → ErrorLevel
─────────────────────────────────────
CRITICAL                → CRITICAL
ERROR                   → ERROR
WARNING                 → WARNING
INFO                    → INFO
DEBUG                   → DEBUG
```

**Example**:
```php
// QueueErrorCode::JOB_NOT_FOUND has severity ERROR
throw JobNotFoundException::byId($jobId);
// → Logged at ERROR level

// QueueErrorCode::INVALID_STATE has severity WARNING
throw InvalidChainStateException::notPending($chainId, $status);
// → Logged at WARNING level
```

### 3. Enhanced Error Metadata

FrameworkException now provides rich metadata automatically:

**Metadata Structure**:
```php
[
    'exception_class' => 'App\Framework\Queue\Exceptions\JobNotFoundException',
    'error_level' => 'ERROR',
    'error_code' => 'QUEUE007',
    'error_category' => 'QUEUE',
    'error_severity' => 'error',
    'is_recoverable' => true,
    'recovery_hint' => 'Verify job ID and check if job was already processed or expired',
    'http_status' => 500,
    'additional_headers' => [
        'Retry-After' => '120'
    ]
]
```

**API Response Example** (Debug Mode):
```json
{
    "error": {
        "code": "QUEUE007",
        "category": "QUEUE",
        "message": "Job with ID 'job-123' not found",
        "severity": "error",
        "recoverable": true,
        "recovery_hint": "Verify job ID and check if job was already processed or expired",
        "context": {
            "operation": "job.lookup",
            "component": "JobPersistenceLayer",
            "data": {
                "job_id": "job-123",
                "search_type": "by_id"
            }
        }
    }
}
```

### 4. Automatic Retry-After Headers

ErrorCode.getRetryAfterSeconds() is automatically converted to HTTP Retry-After headers.

**Example**:
```php
// DatabaseErrorCode::CONNECTION_FAILED returns 30 seconds
throw ConnectionFailedException::toDatabase($config);
// → Response includes: Retry-After: 30

// QueueErrorCode::WORKER_UNAVAILABLE returns 60 seconds
throw WorkerUnavailableException::forQueue($queueName);
// → Response includes: Retry-After: 60
```

**HTTP Response**:
```http
HTTP/1.1 500 Internal Server Error
Retry-After: 30
Content-Type: application/json

{
    "error": {
        "code": "DB001",
        "message": "Database connection failed",
        "retry_after": 30
    }
}
```

### 5. Recovery Hints in Debug Mode

ErrorCode.getRecoveryHint() is included in responses when debug mode is enabled.

**Production Response**:
```json
{
    "error": {
        "code": "QUEUE007",
        "message": "Job not found"
    }
}
```

**Debug Mode Response**:
```json
{
    "error": {
        "code": "QUEUE007",
        "message": "Job with ID 'job-123' not found",
        "recovery_hint": "Verify job ID and check if job was already processed or expired",
        "context": {
            "operation": "job.lookup",
            "component": "JobPersistenceLayer",
            "data": {
                "job_id": "job-123",
                "search_type": "by_id"
            }
        }
    }
}
```

## Implementation Details

### createExceptionMetadata() Enhancement

```php
private function createExceptionMetadata(Throwable $exception): array
{
    $metadata = [
        'exception_class' => get_class($exception),
        'error_level' => $this->determineErrorLevel($exception)->name,
    ];

    // Enhanced: Add ErrorCode metadata if FrameworkException
    if ($exception instanceof FrameworkException) {
        $errorCode = $exception->getErrorCode();
        $metadata['error_code'] = $errorCode->getValue();
        $metadata['error_category'] = $errorCode->getCategory();
        $metadata['error_severity'] = $errorCode->getSeverity()->value;
        $metadata['is_recoverable'] = $errorCode->isRecoverable();

        // Add recovery hint for debug mode
        if ($this->isDebugMode) {
            $metadata['recovery_hint'] = $errorCode->getRecoveryHint();
        }

        // Add Retry-After header if applicable
        $retryAfter = $errorCode->getRetryAfterSeconds();
        if ($retryAfter !== null) {
            $metadata['additional_headers']['Retry-After'] = (string) $retryAfter;
        }
    }

    // HTTP-Status-Code: ErrorCode-based first, then fallback
    $metadata['http_status'] = $this->determineHttpStatus($exception);

    return $metadata;
}
```

### determineHttpStatus() Logic

```php
private function determineHttpStatus(Throwable $exception): int
{
    // Priority 1: ErrorCode-based status mapping for FrameworkException
    if ($exception instanceof FrameworkException) {
        $category = $exception->getErrorCode()->getCategory();

        return match ($category) {
            'AUTH' => 401,  // Authentication errors
            'AUTHZ' => 403, // Authorization errors
            'VAL' => 400,   // Validation errors
            'HTTP' => $this->mapHttpCategoryToStatus($exception),
            'DB', 'QUEUE', 'CACHE', 'FILE' => 500, // Infrastructure
            default => 500,
        };
    }

    // Priority 2: Legacy exception-type mapping (fallback)
    return match (true) {
        $exception instanceof InvalidCredentialsException => 401,
        $exception instanceof InsufficientPrivilegesException => 403,
        $exception instanceof RouteNotFoundException => 404,
        $exception instanceof RateLimitExceededException => 429,
        default => 500,
    };
}
```

### determineErrorLevel() Logic

```php
private function determineErrorLevel(Throwable $exception): ErrorLevel
{
    // Priority 1: Use ErrorCode.getSeverity() for FrameworkException
    if ($exception instanceof FrameworkException) {
        return $this->mapErrorSeverityToErrorLevel(
            $exception->getErrorCode()->getSeverity()
        );
    }

    // Priority 2: Legacy exception-type mapping (fallback)
    return match(true) {
        $exception instanceof \Error => ErrorLevel::CRITICAL,
        $exception instanceof \RuntimeException => ErrorLevel::ERROR,
        default => ErrorLevel::ERROR,
    };
}
```

## Benefits

### For API Clients

✅ **Consistent Error Codes**: Machine-readable error codes (e.g., `QUEUE007`)
✅ **Category-Based Handling**: Handle errors by category (e.g., all `QUEUE` errors)
✅ **Automatic Retry Logic**: Retry-After headers for recoverable errors
✅ **Rich Error Context**: Detailed context for debugging
✅ **Severity Information**: Understand error criticality

### For Developers

✅ **Automatic HTTP Status**: No manual status code mapping needed
✅ **Correct Log Levels**: Severity-based logging automatically
✅ **Recovery Hints**: Actionable hints in debug mode
✅ **Type-Safe Error Handling**: Catch specific exception types
✅ **Self-Documenting Errors**: ErrorCode provides description and hints

### For Operations

✅ **Structured Logging**: Rich context for log aggregation
✅ **Severity-Based Alerting**: Alert on CRITICAL/ERROR severity
✅ **Retry Strategy Support**: Automatic retry hints for infrastructure errors
✅ **Category-Based Monitoring**: Monitor errors by category
✅ **Performance Tracking**: Track error rates by category/severity

## Usage Examples

### Example 1: Queue Job Processing

```php
use App\Framework\Queue\Exceptions\JobNotFoundException;

try {
    $job = $this->jobPersistence->getJobState($jobId);
} catch (JobNotFoundException $e) {
    // ErrorHandler automatically:
    // - Sets HTTP status to 500 (QUEUE category)
    // - Logs at ERROR level (error severity)
    // - Includes recovery hint in debug mode
    // - No Retry-After header (not set for JOB_NOT_FOUND)

    return response()->json([
        'error' => [
            'code' => $e->getErrorCode()->getValue(), // 'QUEUE007'
            'message' => $e->getMessage(),
        ]
    ]);
}
```

### Example 2: Database Connection

```php
use App\Framework\Database\Exceptions\ConnectionFailedException;

try {
    $connection = $this->database->connect($config);
} catch (ConnectionFailedException $e) {
    // ErrorHandler automatically:
    // - Sets HTTP status to 500 (DB category)
    // - Logs at CRITICAL level (critical severity)
    // - Adds Retry-After: 30 header
    // - Includes recovery hint: "Check database server status..."

    throw $e; // Let ErrorHandler handle response
}
```

### Example 3: Authentication

```php
use App\Framework\Auth\Exceptions\InvalidCredentialsException;

try {
    $user = $this->authService->authenticate($credentials);
} catch (InvalidCredentialsException $e) {
    // ErrorHandler automatically:
    // - Sets HTTP status to 401 (AUTH category)
    // - Logs at ERROR level
    // - No Retry-After (not recoverable by retry)

    return response()->json([
        'error' => [
            'code' => $e->getErrorCode()->getValue(),
            'message' => 'Invalid credentials',
        ]
    ], 401);
}
```

### Example 4: Validation Errors

```php
use App\Framework\Validation\Exceptions\ValidationException;

try {
    $validated = $this->validator->validate($data, $rules);
} catch (ValidationException $e) {
    // ErrorHandler automatically:
    // - Sets HTTP status to 400 (VAL category)
    // - Logs at WARNING level
    // - Includes field-specific errors

    return response()->json([
        'error' => [
            'code' => $e->getErrorCode()->getValue(),
            'message' => 'Validation failed',
            'errors' => $e->getErrors(),
        ]
    ], 400);
}
```

## Testing Error Responses

### Test ErrorCode Metadata

```php
it('includes error code metadata in response', function () {
    $jobId = JobId::fromString('test-job-123');

    try {
        throw JobNotFoundException::byId($jobId);
    } catch (JobNotFoundException $e) {
        $response = $this->errorHandler->createHttpResponse($e);

        expect($response->status->value)->toBe(500);
        // Additional metadata assertions...
    }
});
```

### Test Retry-After Headers

```php
it('adds retry-after header for recoverable errors', function () {
    $exception = ConnectionFailedException::toDatabase($config);
    $response = $this->errorHandler->createHttpResponse($exception);

    expect($response->headers)->toHaveKey('Retry-After');
    expect($response->headers['Retry-After'])->toBe('30');
});
```

### Test Debug Mode Recovery Hints

```php
it('includes recovery hint in debug mode', function () {
    $this->errorHandler = new ErrorHandler(
        isDebugMode: true,
        // ... other dependencies
    );

    $exception = JobNotFoundException::byId($jobId);
    $response = $this->errorHandler->createHttpResponse($exception);

    $data = json_decode($response->body, true);
    expect($data['error']['recovery_hint'])->toContain('Verify job ID');
});
```

## Configuration

### Enable Debug Mode

```env
# .env
APP_DEBUG=true  # Enable recovery hints and full error details
```

### Production Settings

```env
# .env
APP_DEBUG=false # Disable recovery hints, sanitize error details
APP_ENV=production
```

## Migration from Legacy Error Handling

### Before (Legacy)

```php
try {
    $job = $this->findJob($jobId);
} catch (\RuntimeException $e) {
    // Manual status code determination
    $status = 500;

    // Manual logging
    $this->logger->error('Job not found', [
        'job_id' => $jobId,
        'message' => $e->getMessage(),
    ]);

    // Manual response creation
    return response()->json([
        'error' => $e->getMessage()
    ], $status);
}
```

### After (Enhanced)

```php
try {
    $job = $this->findJob($jobId);
} catch (JobNotFoundException $e) {
    // ErrorHandler automatically:
    // - Determines HTTP status (500 for QUEUE category)
    // - Logs with correct severity (ERROR level)
    // - Includes rich context and recovery hints

    throw $e; // Let ErrorHandler create response
}
```

## Performance Considerations

### Metadata Extraction Overhead

- **Minimal**: ErrorCode metadata extraction is ~0.1ms per exception
- **Cached**: ErrorLevel mapping uses match expressions (compiled by opcache)
- **Conditional**: Recovery hints only added in debug mode

### Logging Performance

- **Improved**: Correct severity levels reduce log volume
- **Structured**: Rich context enables better log aggregation
- **Filtered**: Production mode filters debug/info messages

## Best Practices

### 1. Let ErrorHandler Handle Responses

```php
// ✅ GOOD: Let ErrorHandler create response
try {
    $result = $this->service->process($data);
} catch (FrameworkException $e) {
    throw $e; // ErrorHandler creates appropriate response
}

// ❌ AVOID: Manual response creation loses enhancements
try {
    $result = $this->service->process($data);
} catch (FrameworkException $e) {
    return response()->json(['error' => $e->getMessage()], 500);
}
```

### 2. Use ErrorCode Categories for Handling

```php
// ✅ GOOD: Category-based error handling
try {
    $result = $this->service->execute();
} catch (FrameworkException $e) {
    if ($e->isCategory('DB')) {
        // Handle database errors
        $this->notifyOps($e);
    }

    throw $e;
}
```

### 3. Trust Automatic Retry-After

```php
// ✅ GOOD: ErrorCode determines retry strategy
catch (FrameworkException $e) {
    // ErrorHandler automatically adds Retry-After if applicable
    throw $e;
}

// ❌ AVOID: Manual retry logic duplicates ErrorCode
catch (ConnectionFailedException $e) {
    header('Retry-After: 30'); // Duplicates ErrorCode logic
}
```

### 4. Use Debug Mode Appropriately

```env
# Development
APP_DEBUG=true

# Staging
APP_DEBUG=true

# Production
APP_DEBUG=false
```

## Troubleshooting

### Error: No HTTP Status Set

**Problem**: Response has no HTTP status
**Cause**: Exception doesn't extend FrameworkException and no legacy mapping exists
**Solution**: Ensure custom exceptions extend FrameworkException

### Error: Wrong Log Level

**Problem**: Errors logged at wrong level
**Cause**: ErrorCode.getSeverity() not implemented correctly
**Solution**: Verify ErrorCode enum implements getSeverity() with correct mapping

### Error: Missing Recovery Hints

**Problem**: No recovery hints in debug mode
**Cause**: Debug mode not enabled
**Solution**: Set `APP_DEBUG=true` in `.env`

### Error: No Retry-After Header

**Problem**: Retry-After header missing for recoverable errors
**Cause**: ErrorCode.getRetryAfterSeconds() returns null
**Solution**: Implement getRetryAfterSeconds() for recoverable errors

## See Also

- [Exception Hierarchy Pattern Guide](./exception-hierarchy-pattern.md)
- [Developer Migration Guide](./exception-migration-guide.md)
- [ErrorCode Reference](./error-code-reference.md)
