Error Handling in PHP: Best Practices for Production

By Maulik Paghdal

06 Dec, 2024

•  11 minutes to Read

Error Handling in PHP: Best Practices for Production

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 LevelImpactAction RequiredProduction Visibility
E_ERROR/E_PARSEApplication crashImmediate fixHidden from users
E_WARNINGLogic errorsHigh priority fixHidden, logged
E_NOTICECode quality issuesCode review fixHidden, optionally logged
E_STRICTStandards complianceBest practice fixHidden, development only
E_DEPRECATEDFuture compatibilityPlanned refactoringHidden, 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

ConfigurationDevelopmentStagingProductionPurpose
display_errors100Show errors to developers only
log_errors111Always log errors
error_reportingE_ALLE_ALLE_ERROR|E_WARNING|E_PARSEControl what gets reported
expose_php100Hide PHP version from headers
Custom error handlerConsistent error handling
Exception handlerCatch uncaught exceptions
Error monitoringOptionalTrack 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

  1. 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;
}
  1. Bugsnag: Comprehensive error monitoring with release tracking
  2. Rollbar: Real-time error alerting with deployment correlation
  3. 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:

  1. Fail fast in development, fail gracefully in production
  2. Log everything important, but don't overwhelm yourself with noise
  3. Always have a fallback plan for critical operations
  4. Monitor your error rates and trends, not just individual errors
  5. 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! 🐘

Topics Covered

About Author

I'm Maulik Paghdal, the founder of Script Binary and a passionate full-stack web developer. I have a strong foundation in both frontend and backend development, specializing in building dynamic, responsive web applications using Laravel, Vue.js, and React.js. With expertise in Tailwind CSS and Bootstrap, I focus on creating clean, efficient, and scalable solutions that enhance user experiences and optimize performance.