1. php
  2. /advanced
  3. /error-handling

PHP Error Handling

Introduction to PHP Error Handling

Proper error handling is crucial for building robust PHP applications. It involves anticipating potential problems, gracefully handling errors when they occur, and providing meaningful feedback to developers and users. Modern PHP emphasizes exception-based error handling over traditional error reporting.

Exception Handling

Basic Exception Handling

<?php
try {
    $result = 10 / 0;
} catch (DivisionByZeroError $e) {
    echo "Error: Cannot divide by zero";
    error_log($e->getMessage());
} catch (Exception $e) {
    echo "An unexpected error occurred";
    error_log($e->getMessage());
} finally {
    echo "This always executes";
}

// Multiple catch blocks
try {
    $pdo = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
    $stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
    $stmt->execute([$userId]);
    
} catch (PDOException $e) {
    echo "Database error: " . $e->getMessage();
} catch (Exception $e) {
    echo "General error: " . $e->getMessage();
}
?>

Custom Exceptions

<?php
// Custom exception classes
class ValidationException extends Exception 
{
    private array $errors = [];
    
    public function __construct(array $errors, string $message = "Validation failed", int $code = 0, ?Throwable $previous = null)
    {
        $this->errors = $errors;
        parent::__construct($message, $code, $previous);
    }
    
    public function getErrors(): array
    {
        return $this->errors;
    }
}

class UserNotFoundException extends Exception 
{
    public function __construct(int $userId, int $code = 404, ?Throwable $previous = null)
    {
        $message = "User with ID {$userId} not found";
        parent::__construct($message, $code, $previous);
    }
}

class InsufficientFundsException extends Exception 
{
    private float $balance;
    private float $attempted;
    
    public function __construct(float $balance, float $attempted, int $code = 0, ?Throwable $previous = null)
    {
        $this->balance = $balance;
        $this->attempted = $attempted;
        
        $message = "Insufficient funds. Balance: {$balance}, Attempted: {$attempted}";
        parent::__construct($message, $code, $previous);
    }
    
    public function getBalance(): float
    {
        return $this->balance;
    }
    
    public function getAttempted(): float
    {
        return $this->attempted;
    }
}

// Usage
class UserService 
{
    public function validateUser(array $data): void
    {
        $errors = [];
        
        if (empty($data['email'])) {
            $errors['email'] = 'Email is required';
        } elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
            $errors['email'] = 'Invalid email format';
        }
        
        if (empty($data['password'])) {
            $errors['password'] = 'Password is required';
        } elseif (strlen($data['password']) < 8) {
            $errors['password'] = 'Password must be at least 8 characters';
        }
        
        if (!empty($errors)) {
            throw new ValidationException($errors);
        }
    }
    
    public function findUser(int $id): User
    {
        $user = $this->repository->find($id);
        
        if (!$user) {
            throw new UserNotFoundException($id);
        }
        
        return $user;
    }
}

// Handling custom exceptions
try {
    $userService = new UserService();
    $userService->validateUser($_POST);
    
} catch (ValidationException $e) {
    $errors = $e->getErrors();
    foreach ($errors as $field => $message) {
        echo "Error in {$field}: {$message}\n";
    }
    
} catch (UserNotFoundException $e) {
    http_response_code(404);
    echo "User not found: " . $e->getMessage();
    
} catch (Exception $e) {
    http_response_code(500);
    echo "An unexpected error occurred";
    error_log($e->getMessage());
}
?>

Error Logging and Monitoring

Comprehensive Error Logger

<?php
class ErrorLogger 
{
    private string $logPath;
    private string $logLevel;
    
    public const EMERGENCY = 'emergency';
    public const ALERT = 'alert';
    public const CRITICAL = 'critical';
    public const ERROR = 'error';
    public const WARNING = 'warning';
    public const NOTICE = 'notice';
    public const INFO = 'info';
    public const DEBUG = 'debug';
    
    public function __construct(string $logPath = '/var/log/app/', string $logLevel = self::ERROR)
    {
        $this->logPath = rtrim($logPath, '/') . '/';
        $this->logLevel = $logLevel;
        
        if (!is_dir($this->logPath)) {
            mkdir($this->logPath, 0755, true);
        }
    }
    
    public function log(string $level, string $message, array $context = []): void
    {
        $logEntry = [
            'timestamp' => date('Y-m-d H:i:s'),
            'level' => strtoupper($level),
            'message' => $message,
            'context' => $context,
            'memory_usage' => memory_get_usage(true),
            'memory_peak' => memory_get_peak_usage(true)
        ];
        
        if (isset($context['exception']) && $context['exception'] instanceof Throwable) {
            $logEntry['exception'] = [
                'class' => get_class($context['exception']),
                'message' => $context['exception']->getMessage(),
                'code' => $context['exception']->getCode(),
                'file' => $context['exception']->getFile(),
                'line' => $context['exception']->getLine(),
                'trace' => $context['exception']->getTraceAsString()
            ];
        }
        
        $filename = $this->logPath . date('Y-m-d') . '.log';
        $logLine = json_encode($logEntry) . "\n";
        
        file_put_contents($filename, $logLine, FILE_APPEND | LOCK_EX);
    }
    
    public function error(string $message, array $context = []): void
    {
        $this->log(self::ERROR, $message, $context);
    }
    
    public function warning(string $message, array $context = []): void
    {
        $this->log(self::WARNING, $message, $context);
    }
    
    public function info(string $message, array $context = []): void
    {
        $this->log(self::INFO, $message, $context);
    }
    
    public function debug(string $message, array $context = []): void
    {
        $this->log(self::DEBUG, $message, $context);
    }
    
    public function logException(Throwable $exception, array $context = []): void
    {
        $context['exception'] = $exception;
        $this->error('Uncaught exception: ' . $exception->getMessage(), $context);
    }
}

// Usage
$logger = new ErrorLogger('/var/log/myapp/');

try {
    $user = $userService->findUser(123);
} catch (UserNotFoundException $e) {
    $logger->logException($e, ['user_id' => 123, 'action' => 'find_user']);
    throw $e; // Re-throw if needed
} catch (Exception $e) {
    $logger->logException($e, ['context' => 'user_lookup']);
    throw $e;
}

$logger->info('User login successful', ['user_id' => 456, 'ip' => $_SERVER['REMOTE_ADDR']]);
$logger->warning('Rate limit approaching', ['user_id' => 789, 'requests' => 95]);
?>

Global Exception Handler

<?php
class GlobalExceptionHandler 
{
    private ErrorLogger $logger;
    private bool $displayErrors;
    
    public function __construct(ErrorLogger $logger, bool $displayErrors = false)
    {
        $this->logger = $logger;
        $this->displayErrors = $displayErrors;
        
        set_exception_handler([$this, 'handleException']);
        set_error_handler([$this, 'handleError']);
        register_shutdown_function([$this, 'handleFatalError']);
    }
    
    public function handleException(Throwable $exception): void
    {
        $this->logger->logException($exception, [
            'url' => $_SERVER['REQUEST_URI'] ?? 'CLI',
            'method' => $_SERVER['REQUEST_METHOD'] ?? 'CLI',
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'CLI',
            'ip' => $_SERVER['REMOTE_ADDR'] ?? 'localhost'
        ]);
        
        if ($this->displayErrors) {
            $this->displayException($exception);
        } else {
            $this->displayGenericError();
        }
        
        exit(1);
    }
    
    public function handleError(int $severity, string $message, string $file, int $line): bool
    {
        if (!(error_reporting() & $severity)) {
            return false;
        }
        
        $errorException = new ErrorException($message, 0, $severity, $file, $line);
        $this->handleException($errorException);
        
        return true;
    }
    
    public function handleFatalError(): void
    {
        $error = error_get_last();
        
        if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
            $exception = new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line']);
            $this->handleException($exception);
        }
    }
    
    private function displayException(Throwable $exception): void
    {
        http_response_code(500);
        
        echo "<h1>Application Error</h1>";
        echo "<p><strong>Message:</strong> " . htmlspecialchars($exception->getMessage()) . "</p>";
        echo "<p><strong>File:</strong> " . htmlspecialchars($exception->getFile()) . "</p>";
        echo "<p><strong>Line:</strong> " . $exception->getLine() . "</p>";
        echo "<pre>" . htmlspecialchars($exception->getTraceAsString()) . "</pre>";
    }
    
    private function displayGenericError(): void
    {
        http_response_code(500);
        
        echo "<h1>Server Error</h1>";
        echo "<p>An unexpected error occurred. Please try again later.</p>";
        echo "<p>If the problem persists, please contact support.</p>";
    }
}

// Initialize global error handling
$logger = new ErrorLogger('/var/log/myapp/');
$displayErrors = $_ENV['APP_ENV'] === 'development';
$errorHandler = new GlobalExceptionHandler($logger, $displayErrors);
?>

Debugging and Development Tools

Debug Helper Class

<?php
class DebugHelper 
{
    private static bool $enabled = false;
    private static array $timers = [];
    private static array $memory = [];
    
    public static function enable(): void
    {
        self::$enabled = true;
        
        if (function_exists('xdebug_start_trace')) {
            xdebug_start_trace();
        }
    }
    
    public static function disable(): void
    {
        self::$enabled = false;
    }
    
    public static function dump($variable, string $label = ''): void
    {
        if (!self::$enabled) return;
        
        echo "<div style='background: #f0f0f0; border: 1px solid #ccc; padding: 10px; margin: 10px;'>";
        if ($label) {
            echo "<h4>Debug: " . htmlspecialchars($label) . "</h4>";
        }
        echo "<pre>";
        var_dump($variable);
        echo "</pre>";
        echo "</div>";
    }
    
    public static function trace(string $message = ''): void
    {
        if (!self::$enabled) return;
        
        $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
        
        echo "<div style='background: #ffe0e0; border: 1px solid #ff0000; padding: 10px; margin: 10px;'>";
        echo "<h4>Debug Trace" . ($message ? ": " . htmlspecialchars($message) : "") . "</h4>";
        
        foreach ($trace as $i => $step) {
            if ($i === 0) continue; // Skip this function call
            
            echo "<p><strong>#{$i}:</strong> ";
            if (isset($step['file'])) {
                echo htmlspecialchars($step['file']) . ":" . $step['line'] . " ";
            }
            if (isset($step['class'])) {
                echo htmlspecialchars($step['class']) . $step['type'];
            }
            echo htmlspecialchars($step['function']) . "()</p>";
        }
        echo "</div>";
    }
    
    public static function startTimer(string $name): void
    {
        if (!self::$enabled) return;
        
        self::$timers[$name] = microtime(true);
    }
    
    public static function endTimer(string $name): ?float
    {
        if (!self::$enabled || !isset(self::$timers[$name])) return null;
        
        $elapsed = microtime(true) - self::$timers[$name];
        unset(self::$timers[$name]);
        
        echo "<div style='background: #e0f0ff; border: 1px solid #0066cc; padding: 5px; margin: 5px;'>";
        echo "<strong>Timer '{$name}':</strong> " . number_format($elapsed * 1000, 3) . " ms";
        echo "</div>";
        
        return $elapsed;
    }
    
    public static function memoryUsage(string $label = ''): void
    {
        if (!self::$enabled) return;
        
        $current = memory_get_usage(true);
        $peak = memory_get_peak_usage(true);
        
        echo "<div style='background: #f0f0e0; border: 1px solid #cccc00; padding: 5px; margin: 5px;'>";
        echo "<strong>Memory" . ($label ? " ({$label})" : "") . ":</strong> ";
        echo "Current: " . self::formatBytes($current) . ", ";
        echo "Peak: " . self::formatBytes($peak);
        echo "</div>";
    }
    
    private static function formatBytes(int $bytes): string
    {
        $units = ['B', 'KB', 'MB', 'GB'];
        
        for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
            $bytes /= 1024;
        }
        
        return round($bytes, 2) . ' ' . $units[$i];
    }
    
    public static function sql(string $query, array $params = []): void
    {
        if (!self::$enabled) return;
        
        echo "<div style='background: #e0ffe0; border: 1px solid #00cc00; padding: 10px; margin: 10px;'>";
        echo "<h4>SQL Query</h4>";
        echo "<pre>" . htmlspecialchars($query) . "</pre>";
        
        if (!empty($params)) {
            echo "<h5>Parameters:</h5>";
            echo "<pre>" . htmlspecialchars(print_r($params, true)) . "</pre>";
        }
        echo "</div>";
    }
}

// Usage examples
if ($_ENV['APP_ENV'] === 'development') {
    DebugHelper::enable();
}

DebugHelper::startTimer('user_query');
$users = $userRepository->findAll();
DebugHelper::endTimer('user_query');

DebugHelper::dump($users, 'All Users');
DebugHelper::memoryUsage('After loading users');
DebugHelper::trace('Checkpoint in user processing');
?>

Error Recovery and Resilience

Circuit Breaker Pattern

<?php
class CircuitBreaker 
{
    private string $name;
    private int $failureThreshold;
    private int $timeoutDuration;
    private int $failureCount = 0;
    private ?int $lastFailureTime = null;
    private string $state = 'closed'; // closed, open, half-open
    
    public function __construct(string $name, int $failureThreshold = 5, int $timeoutDuration = 60)
    {
        $this->name = $name;
        $this->failureThreshold = $failureThreshold;
        $this->timeoutDuration = $timeoutDuration;
    }
    
    public function call(callable $operation)
    {
        if ($this->state === 'open') {
            if (time() - $this->lastFailureTime >= $this->timeoutDuration) {
                $this->state = 'half-open';
                $this->failureCount = 0;
            } else {
                throw new CircuitBreakerOpenException("Circuit breaker '{$this->name}' is open");
            }
        }
        
        try {
            $result = $operation();
            $this->onSuccess();
            return $result;
            
        } catch (Exception $e) {
            $this->onFailure();
            throw $e;
        }
    }
    
    private function onSuccess(): void
    {
        $this->failureCount = 0;
        $this->state = 'closed';
    }
    
    private function onFailure(): void
    {
        $this->failureCount++;
        $this->lastFailureTime = time();
        
        if ($this->failureCount >= $this->failureThreshold) {
            $this->state = 'open';
        }
    }
    
    public function getState(): string
    {
        return $this->state;
    }
    
    public function getFailureCount(): int
    {
        return $this->failureCount;
    }
}

class CircuitBreakerOpenException extends Exception {}

// Usage
$circuitBreaker = new CircuitBreaker('database', 3, 30);

try {
    $result = $circuitBreaker->call(function() {
        // Potentially failing operation
        return $this->database->query('SELECT * FROM users');
    });
    
} catch (CircuitBreakerOpenException $e) {
    // Fallback behavior when circuit is open
    $result = $this->getCachedUsers();
    
} catch (Exception $e) {
    // Handle other exceptions
    $this->logger->error('Database operation failed', ['exception' => $e]);
    throw $e;
}
?>

Summary

Effective PHP error handling involves:

  • Exception-Based Handling: Use try-catch blocks and custom exceptions
  • Comprehensive Logging: Log errors with context and stack traces
  • Global Handlers: Set up application-wide error and exception handlers
  • Development Tools: Use debugging helpers and profiling tools
  • Recovery Patterns: Implement circuit breakers and fallback mechanisms

Key principles:

  • Fail fast and fail safely
  • Log everything but display selectively
  • Provide meaningful error messages
  • Implement graceful degradation
  • Monitor and alert on error patterns

Proper error handling improves application reliability, simplifies debugging, and enhances user experience by preventing unexpected crashes and providing helpful feedback.