Files
michaelschiemer/docs/claude/error-handling.md
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

5.8 KiB

Error Handling & Debugging

This guide covers error handling patterns and debugging strategies in the framework.

Exception Handling

All custom exceptions in the framework must extend FrameworkException to ensure consistent error handling, logging, and recovery mechanisms.

The FrameworkException System

The framework provides a sophisticated exception system with:

  • ExceptionContext: Rich context information for debugging
  • ErrorCode: Categorized error codes with recovery hints
  • RetryAfter: Support for recoverable operations
  • Fluent Interface: Easy context building

Creating Custom Exceptions

namespace App\Domain\User\Exceptions;

use App\Framework\Exception\FrameworkException;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\ExceptionContext;

final class UserNotFoundException extends FrameworkException
{
    public static function byId(UserId $id): self
    {
        return self::create(
            ErrorCode::ENTITY_NOT_FOUND,
            "User with ID '{$id->toString()}' not found"
        )->withData([
            'user_id' => $id->toString(),
            'search_type' => 'by_id'
        ]);
    }
    
    public static function byEmail(Email $email): self
    {
        $context = ExceptionContext::forOperation('user.lookup', 'UserRepository')
            ->withData(['email' => $email->getMasked()]);
            
        return self::fromContext(
            "User with email not found",
            $context,
            ErrorCode::ENTITY_NOT_FOUND
        );
    }
}

Using ErrorCode Enum

// The framework provides predefined error codes:
ErrorCode::DB_CONNECTION_FAILED      // Database errors
ErrorCode::AUTH_TOKEN_EXPIRED        // Authentication errors
ErrorCode::VAL_BUSINESS_RULE_VIOLATION // Validation errors
ErrorCode::HTTP_RATE_LIMIT_EXCEEDED  // HTTP errors
ErrorCode::SEC_CSRF_TOKEN_INVALID    // Security errors

// Using error codes in exceptions:
throw FrameworkException::create(
    ErrorCode::DB_QUERY_FAILED,
    "Failed to execute user query"
)->withContext(
    ExceptionContext::forOperation('user.find', 'UserRepository')
        ->withData(['query' => 'SELECT * FROM users WHERE id = ?'])
        ->withDebug(['bind_params' => [$userId]])
);

Exception Context Building

// Method 1: Using factory methods
$exception = FrameworkException::forOperation(
    'payment.process',
    'PaymentService',
    'Payment processing failed',
    ErrorCode::PAYMENT_GATEWAY_ERROR
)->withData([
    'amount' => $amount->toArray(),
    'gateway' => 'stripe',
    'customer_id' => $customerId
])->withMetadata([
    'attempt' => 1,
    'idempotency_key' => $idempotencyKey
]);

// Method 2: Building context separately
$context = ExceptionContext::empty()
    ->withOperation('order.validate', 'OrderService')
    ->withData([
        'order_id' => $orderId,
        'total' => $total->toDecimal()
    ])
    ->withDebug([
        'validation_rules' => ['min_amount', 'max_items'],
        'failed_rule' => 'min_amount'
    ]);

throw FrameworkException::fromContext(
    'Order validation failed',
    $context,
    ErrorCode::VAL_BUSINESS_RULE_VIOLATION
);

Recoverable Exceptions

// Creating recoverable exceptions with retry hints
final class RateLimitException extends FrameworkException
{
    public static function exceeded(int $retryAfter): self
    {
        return self::create(
            ErrorCode::HTTP_RATE_LIMIT_EXCEEDED,
            'API rate limit exceeded'
        )->withRetryAfter($retryAfter)
         ->withData(['retry_after_seconds' => $retryAfter]);
    }
}

// Using in code
try {
    $response = $apiClient->request($endpoint);
} catch (RateLimitException $e) {
    if ($e->isRecoverable()) {
        $waitTime = $e->getRetryAfter();
        // Schedule retry after $waitTime seconds
    }
    throw $e;
}

Exception Categories

// Check exception category for handling strategies
try {
    $result = $operation->execute();
} catch (FrameworkException $e) {
    if ($e->isCategory('AUTH')) {
        // Handle authentication errors
        return $this->redirectToLogin();
    }
    
    if ($e->isCategory('VAL')) {
        // Handle validation errors
        return $this->validationErrorResponse($e);
    }
    
    if ($e->isErrorCode(ErrorCode::DB_CONNECTION_FAILED)) {
        // Handle specific database connection errors
        $this->notifyOps($e);
    }
    
    throw $e;
}

Simple Exceptions for Quick Use

// When you don't need the full context system
throw FrameworkException::simple('Quick error message');

// With previous exception
} catch (\PDOException $e) {
    throw FrameworkException::simple(
        'Database operation failed',
        $e,
        500
    );
}

Exception Data Sanitization

The framework automatically sanitizes sensitive data in exceptions:

// Sensitive keys are automatically redacted
$exception->withData([
    'username' => 'john@example.com',
    'password' => 'secret123',  // Will be logged as '[REDACTED]'
    'api_key' => 'sk_live_...' // Will be logged as '[REDACTED]'
]);

Best Practices

  1. Always extend FrameworkException for custom exceptions
  2. Use ErrorCode enum for categorizable errors
  3. Provide rich context with operation, component, and data
  4. Use factory methods for consistent exception creation
  5. Sanitize sensitive data (automatic for common keys)
  6. Make exceptions domain-specific (UserNotFoundException vs generic NotFoundException)
  7. Include recovery hints for recoverable errors

Logging Best Practices

TODO: Document logging patterns and levels

Debug Strategies

TODO: Document debugging approaches and tools

Error Recovery Patterns

TODO: Document error recovery and graceful degradation

Common Error Scenarios

TODO: List common errors and solutions