Introduction
Error handling in PHP isn't just about catching exceptions—it's about crafting a safety net that catches problems before they reach your users, provides meaningful feedback to developers, and maintains the integrity of your application under pressure. This comprehensive guide will walk you through the battle-tested strategies I've learned from building and maintaining production PHP applications that serve thousands of users daily.
What makes error handling particularly crucial in PHP? Unlike compiled languages that catch many issues at compile time, PHP's dynamic nature means errors can surface at runtime in unexpected ways. A single unhandled error can bring down your entire application, expose sensitive information, or worse—silently corrupt data while appearing to work correctly.
Understanding the PHP Error Ecosystem
Types of PHP Errors: More Than Meets the Eye
PHP's error system is more nuanced than many developers realize. Let me break down the error hierarchy based on real-world scenarios I've encountered:
1. Parse Errors (E_PARSE)
These are the "show stoppers"—your script won't even begin execution.
<?php
// This will cause a parse error
if ($condition { // Missing closing parenthesis
echo "Hello World";
}
Real-world scenario: I once deployed code where a merge conflict left unclosed brackets. The entire application was down until the syntax was fixed. Parse errors are why automated testing and proper CI/CD pipelines are non-negotiable.
2. Fatal Errors (E_ERROR)
These halt script execution immediately and can't be caught by user-defined error handlers.
<?php
// Fatal error: calling undefined function
nonExistentFunction(); // Script stops here
echo "This line will never execute";
Pro tip: Modern PHP (7.0+) converts many fatal errors to Error
exceptions, which can be caught. This was a game-changer for error handling strategies.
3. Warning Errors (E_WARNING)
These indicate problems but allow script execution to continue—often the most dangerous type.
<?php
// Warning: file doesn't exist, but script continues
$content = file_get_contents('nonexistent-file.txt');
echo $content; // This will echo nothing, potentially breaking logic
The danger: I've seen applications appear to work correctly while silently failing due to unhandled warnings. Always treat warnings seriously in business-critical applications.
4. Notice Errors (E_NOTICE)
Minor issues that experienced developers often ignore—but shouldn't.
<?php
// Notice: undefined variable
echo $undefinedVariable; // Works, but indicates poor code quality
Why notices matter: In a team environment, notices often indicate sloppy coding practices that can lead to more serious issues down the line.
5. Strict Standards (E_STRICT)
These help maintain forward compatibility and coding standards.
<?php
// Strict Standards: accessing static methods non-statically
class MyClass {
public static function myMethod() {
return "Hello";
}
}
$obj = new MyClass();
echo $obj->myMethod(); // Works but triggers E_STRICT
Error Severity Levels: A Practical Framework
Error Level | Impact | Action Required | Production Visibility |
---|---|---|---|
E_ERROR/E_PARSE | Application crash | Immediate fix | Hidden from users |
E_WARNING | Logic errors | High priority fix | Hidden, logged |
E_NOTICE | Code quality issues | Code review fix | Hidden, optionally logged |
E_STRICT | Standards compliance | Best practice fix | Hidden, development only |
E_DEPRECATED | Future compatibility | Planned refactoring | Hidden, logged for planning |
Environment-Specific Error Handling Strategies
Development Environment: Maximum Visibility
In development, you want to see everything. Here's my recommended setup:
<?php
// development-config.php
if ($_ENV['APP_ENV'] === 'development') {
// Show all errors immediately
error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
// Also log for reference
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/logs/php-errors.log');
// Enable assertions for debugging
assert_options(ASSERT_ACTIVE, 1);
assert_options(ASSERT_WARNING, 1);
}
Pro tip: Use different error reporting levels for different development phases. During initial development, use E_ALL
. During code review, temporarily enable E_STRICT
to catch coding standard issues.
Staging Environment: Production Simulation
Staging should mirror production but with enhanced logging:
<?php
// staging-config.php
if ($_ENV['APP_ENV'] === 'staging') {
// Hide errors from users but log everything
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php/staging-errors.log');
// Enable detailed logging for debugging
ini_set('log_errors_max_len', 0); // No limit on error message length
}
Production Environment: Security and Stability First
Production requires a careful balance between information gathering and security:
<?php
// production-config.php
if ($_ENV['APP_ENV'] === 'production') {
// Log important errors, hide from users
error_reporting(E_ERROR | E_WARNING | E_PARSE);
ini_set('display_errors', 0);
ini_set('display_startup_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php/production-errors.log');
// Security: don't expose PHP version or paths
ini_set('expose_php', 0);
}
Advanced Error Handling Techniques
Custom Error Handlers: Your First Line of Defense
A well-designed custom error handler can transform how you deal with errors. Here's a production-ready implementation:
<?php
class ErrorHandler
{
private $logger;
private $environment;
public function __construct($logger, $environment = 'production')
{
$this->logger = $logger;
$this->environment = $environment;
}
public function handleError($severity, $message, $file, $line, $context = [])
{
// Don't handle errors suppressed with @
if (!(error_reporting() & $severity)) {
return false;
}
$errorInfo = [
'severity' => $this->getSeverityName($severity),
'message' => $message,
'file' => $file,
'line' => $line,
'timestamp' => date('Y-m-d H:i:s'),
'request_uri' => $_SERVER['REQUEST_URI'] ?? 'CLI',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'CLI',
'memory_usage' => memory_get_usage(true),
];
// Add context in development
if ($this->environment === 'development') {
$errorInfo['context'] = $context;
$errorInfo['backtrace'] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
}
$this->logger->error('PHP Error', $errorInfo);
// For critical errors in production, notify immediately
if ($severity & (E_ERROR | E_PARSE | E_CORE_ERROR)) {
$this->notifyAdministrators($errorInfo);
}
return true; // Don't execute PHP's internal error handler
}
private function getSeverityName($severity)
{
$severityNames = [
E_ERROR => 'ERROR',
E_WARNING => 'WARNING',
E_PARSE => 'PARSE',
E_NOTICE => 'NOTICE',
E_CORE_ERROR => 'CORE_ERROR',
E_CORE_WARNING => 'CORE_WARNING',
E_COMPILE_ERROR => 'COMPILE_ERROR',
E_USER_ERROR => 'USER_ERROR',
E_USER_WARNING => 'USER_WARNING',
E_USER_NOTICE => 'USER_NOTICE',
E_STRICT => 'STRICT',
E_RECOVERABLE_ERROR => 'RECOVERABLE_ERROR',
E_DEPRECATED => 'DEPRECATED',
];
return $severityNames[$severity] ?? 'UNKNOWN';
}
private function notifyAdministrators($errorInfo)
{
// Implement your notification system here
// Could be email, Slack, SMS, etc.
mail('admin@example.com', 'Critical PHP Error', json_encode($errorInfo));
}
}
// Register the error handler
$errorHandler = new ErrorHandler($logger, $_ENV['APP_ENV']);
set_error_handler([$errorHandler, 'handleError']);
Exception Handling: Beyond Basic Try-Catch
Exception handling in modern PHP goes far beyond simple try-catch blocks. Here's how to build a robust exception handling system:
<?php
// Custom exception hierarchy
abstract class ApplicationException extends Exception
{
protected $context = [];
public function __construct($message = "", $code = 0, Throwable $previous = null, array $context = [])
{
parent::__construct($message, $code, $previous);
$this->context = $context;
}
public function getContext(): array
{
return $this->context;
}
abstract public function getLogLevel(): string;
abstract public function getUserMessage(): string;
}
class DatabaseException extends ApplicationException
{
public function getLogLevel(): string
{
return 'error';
}
public function getUserMessage(): string
{
return 'We are experiencing technical difficulties. Please try again later.';
}
}
class ValidationException extends ApplicationException
{
public function getLogLevel(): string
{
return 'warning';
}
public function getUserMessage(): string
{
return $this->getMessage(); // Validation messages are safe to show users
}
}
class BusinessLogicException extends ApplicationException
{
public function getLogLevel(): string
{
return 'info';
}
public function getUserMessage(): string
{
return $this->getMessage();
}
}
Global Exception Handler: Your Safety Net
<?php
class GlobalExceptionHandler
{
private $logger;
private $environment;
public function __construct($logger, $environment)
{
$this->logger = $logger;
$this->environment = $environment;
}
public function handleException(Throwable $exception)
{
$exceptionData = [
'type' => get_class($exception),
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'code' => $exception->getCode(),
'timestamp' => date('Y-m-d H:i:s'),
'request_uri' => $_SERVER['REQUEST_URI'] ?? 'CLI',
'request_method' => $_SERVER['REQUEST_METHOD'] ?? 'CLI',
];
// Add context for custom exceptions
if ($exception instanceof ApplicationException) {
$exceptionData['context'] = $exception->getContext();
$logLevel = $exception->getLogLevel();
$userMessage = $exception->getUserMessage();
} else {
$logLevel = 'error';
$userMessage = 'An unexpected error occurred. Please try again later.';
}
// Log the exception
$this->logger->{$logLevel}('Uncaught Exception', $exceptionData);
// Show appropriate message to user
if ($this->environment === 'development') {
$this->displayDevelopmentError($exception);
} else {
$this->displayProductionError($userMessage);
}
// Clean up and exit gracefully
$this->cleanup();
exit(1);
}
private function displayDevelopmentError(Throwable $exception)
{
echo "<h1>Exception: " . get_class($exception) . "</h1>";
echo "<p><strong>Message:</strong> " . htmlspecialchars($exception->getMessage()) . "</p>";
echo "<p><strong>File:</strong> " . $exception->getFile() . " (Line: " . $exception->getLine() . ")</p>";
echo "<h2>Stack Trace:</h2>";
echo "<pre>" . htmlspecialchars($exception->getTraceAsString()) . "</pre>";
}
private function displayProductionError($message)
{
http_response_code(500);
include 'error-pages/500.php'; // Your custom error page
}
private function cleanup()
{
// Close database connections, clean up resources, etc.
if (isset($GLOBALS['pdo'])) {
$GLOBALS['pdo'] = null;
}
}
}
// Register the exception handler
$exceptionHandler = new GlobalExceptionHandler($logger, $_ENV['APP_ENV']);
set_exception_handler([$exceptionHandler, 'handleException']);
Real-World Error Handling Patterns
The Circuit Breaker Pattern
When dealing with external services, implement a circuit breaker to prevent cascading failures:
<?php
class CircuitBreaker
{
private $failureCount = 0;
private $lastFailureTime = null;
private $threshold = 5;
private $timeout = 60; // seconds
public function call(callable $operation)
{
if ($this->isOpen()) {
throw new Exception('Circuit breaker is open');
}
try {
$result = $operation();
$this->onSuccess();
return $result;
} catch (Exception $e) {
$this->onFailure();
throw $e;
}
}
private function isOpen(): bool
{
return $this->failureCount >= $this->threshold &&
(time() - $this->lastFailureTime) < $this->timeout;
}
private function onSuccess()
{
$this->failureCount = 0;
$this->lastFailureTime = null;
}
private function onFailure()
{
$this->failureCount++;
$this->lastFailureTime = time();
}
}
Graceful Degradation Strategy
<?php
class UserService
{
private $primaryDb;
private $cacheService;
private $logger;
public function getUser($userId)
{
try {
// Try primary database first
return $this->primaryDb->getUser($userId);
} catch (DatabaseException $e) {
$this->logger->warning('Primary DB failed, trying cache', ['user_id' => $userId]);
try {
// Fall back to cache
$user = $this->cacheService->get("user:$userId");
if ($user) {
return $user;
}
} catch (Exception $cacheException) {
$this->logger->error('Cache also failed', ['user_id' => $userId]);
}
// Last resort: return default user object
return $this->getDefaultUser($userId);
}
}
private function getDefaultUser($userId)
{
return new User($userId, 'Guest User', 'guest@example.com');
}
}
Error Monitoring and Alerting
Setting Up Comprehensive Monitoring
Modern error monitoring goes beyond simple log files. Here's a comprehensive monitoring setup:
<?php
class ErrorMonitor
{
private $alertThresholds = [
'error_rate' => 0.05, // 5% error rate triggers alert
'response_time' => 2.0, // 2 second response time
'memory_usage' => 0.8, // 80% memory usage
];
private $metrics = [];
public function recordError($error)
{
$this->metrics['errors'][] = [
'timestamp' => time(),
'type' => $error['type'],
'severity' => $error['severity'],
];
$this->checkThresholds();
}
public function recordRequest($responseTime, $memoryUsage)
{
$this->metrics['requests'][] = [
'timestamp' => time(),
'response_time' => $responseTime,
'memory_usage' => $memoryUsage,
];
$this->checkThresholds();
}
private function checkThresholds()
{
$errorRate = $this->calculateErrorRate();
if ($errorRate > $this->alertThresholds['error_rate']) {
$this->sendAlert("High error rate detected: {$errorRate}%");
}
$avgResponseTime = $this->calculateAverageResponseTime();
if ($avgResponseTime > $this->alertThresholds['response_time']) {
$this->sendAlert("High response time detected: {$avgResponseTime}s");
}
}
private function calculateErrorRate()
{
$recentRequests = $this->getRecentRequests(300); // Last 5 minutes
$recentErrors = $this->getRecentErrors(300);
if (count($recentRequests) === 0) return 0;
return (count($recentErrors) / count($recentRequests)) * 100;
}
private function sendAlert($message)
{
// Implement your alerting system
// Slack, email, SMS, PagerDuty, etc.
}
}
Production-Ready Error Handling Checklist
Essential Configuration Checklist
Configuration | Development | Staging | Production | Purpose |
---|---|---|---|---|
display_errors | 1 | 0 | 0 | Show errors to developers only |
log_errors | 1 | 1 | 1 | Always log errors |
error_reporting | E_ALL | E_ALL | E_ERROR|E_WARNING|E_PARSE | Control what gets reported |
expose_php | 1 | 0 | 0 | Hide PHP version from headers |
Custom error handler | ✓ | ✓ | ✓ | Consistent error handling |
Exception handler | ✓ | ✓ | ✓ | Catch uncaught exceptions |
Error monitoring | Optional | ✓ | ✓ | Track error trends |
Security Considerations
Never expose these in production:
- Database connection strings
- File system paths
- Stack traces with sensitive data
- Internal system information
- User data in error messages
Always implement:
- Input validation and sanitization
- Proper error message sanitization
- Rate limiting for error-prone endpoints
- Secure error logging (proper file permissions)
Advanced Tools and Integration
Popular Error Monitoring Solutions
- Sentry: Real-time error tracking with detailed context
use Sentry\init;
init(['dsn' => 'YOUR_SENTRY_DSN']);
try {
riskyOperation();
} catch (Exception $e) {
\Sentry\captureException($e);
throw $e;
}
- Bugsnag: Comprehensive error monitoring with release tracking
- Rollbar: Real-time error alerting with deployment correlation
- New Relic: Application performance monitoring with error tracking
Development Tools for Better Error Handling
Whoops - Beautiful error pages for development:
$whoops = new \Whoops\Run;
$whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
$whoops->register();
Monolog - Flexible logging library:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
$logger = new Logger('app');
$logger->pushHandler(new StreamHandler('php://stdout', Logger::WARNING));
$logger->pushHandler(new RotatingFileHandler('/path/to/logs/app.log', 0, Logger::DEBUG));
Testing Your Error Handling
Unit Testing Error Scenarios
<?php
class ErrorHandlingTest extends PHPUnit\Framework\TestCase
{
public function testDatabaseConnectionFailure()
{
$mockDb = $this->createMock(PDO::class);
$mockDb->method('query')->willThrowException(new PDOException('Connection failed'));
$service = new UserService($mockDb);
$this->expectException(DatabaseException::class);
$service->getUser(123);
}
public function testGracefulDegradationOnError()
{
$mockDb = $this->createMock(PDO::class);
$mockDb->method('query')->willThrowException(new PDOException('Connection failed'));
$mockCache = $this->createMock(CacheInterface::class);
$mockCache->method('get')->willReturn(new User(123, 'Cached User'));
$service = new UserService($mockDb, $mockCache);
$user = $service->getUser(123);
$this->assertEquals('Cached User', $user->getName());
}
}
Conclusion
The strategies outlined in this guide have saved me countless hours of debugging and prevented numerous outages. Remember these key principles:
- Fail fast in development, fail gracefully in production
- Log everything important, but don't overwhelm yourself with noise
- Always have a fallback plan for critical operations
- Monitor your error rates and trends, not just individual errors
- Security first—never expose sensitive information in error messages
Start implementing these practices incrementally. Begin with proper error logging, then add custom error handlers, and gradually build up to comprehensive monitoring and alerting. Your future self (and your team) will thank you when that 2 AM alert comes in, and you can quickly identify and resolve the issue instead of spending hours debugging in the dark.
Remember, good error handling is like insurance—you hope you never need it, but when you do, you'll be grateful you invested in it early.
Happy coding, and may your logs be informative and your errors be gracefully handled! 🐘