Tutorial

PHP Error Handling: Display Errors, Log Errors, and Debug

May 11, 2026

Back to Blog

Why PHP Error Handling Matters

Every PHP developer has experienced it: you deploy your code, a user reports a blank white page, and you have absolutely no idea what went wrong. The page just... stops rendering. No error message, no stack trace, nothing. This is the infamous "White Screen of Death," and it happens because PHP's error handling was not configured correctly.

Proper error handling is the difference between a developer who can diagnose and fix issues in minutes and one who spends hours guessing. In development, you want every error screaming at you in the browser. In production, you want errors silently logged to files where you can review them, while users see a friendly error page instead of raw stack traces that expose your code structure.

This guide covers everything you need to know about PHP error handling: the error reporting levels, the critical display_errors vs log_errors settings, custom error and exception handlers, structured logging, and the tools and techniques that make debugging PHP applications efficient.

What you will learn: PHP error types and reporting levels, configuring php.ini for development and production, custom error handlers, try/catch/finally patterns, Xdebug setup, log rotation, structured logging, and common PHP errors with their solutions.

PHP Error Reporting Levels

PHP categorizes errors into different levels, each represented by a predefined constant. Understanding these levels is essential for knowing what gets reported and what gets silently ignored.

ConstantValueDescriptionSeverity
E_ERROR1Fatal runtime error, script haltsFatal
E_WARNING2Non-fatal runtime warningWarning
E_PARSE4Syntax error at compile timeFatal
E_NOTICE8Minor issue (undefined variable, etc.)Notice
E_STRICT2048Suggestions for code improvementNotice
E_DEPRECATED8192Code that will break in future PHP versionsWarning
E_ALL~0All errors, warnings, and noticesAll

The error_reporting directive accepts a bitmask of these constants. You combine them with bitwise operators:

// Report all errors (recommended for development) error_reporting(E_ALL); // Report all errors except notices and deprecation warnings error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED); // Report only fatal errors and warnings error_reporting(E_ERROR | E_WARNING | E_PARSE); // Turn off all error reporting (never do this!) error_reporting(0);
Best practice: Always use E_ALL during development. In production, use E_ALL as well but with display_errors = Off and log_errors = On. Never suppress error reporting — you want to know about every issue, even if users do not see them.

The Two Critical Settings: display_errors vs log_errors

These two php.ini directives are the most important settings for error handling, and they serve completely different purposes:

display_errors

Controls whether errors are shown in the browser output. In development, set this to On so you see errors immediately. In production, set this to Offalways. Displaying errors to users exposes file paths, database credentials, and code structure.

Dev: On   Prod: Off

log_errors

Controls whether errors are written to a log file. This should be On in both environments. Combined with the error_log directive, this ensures every error is captured somewhere you can review it.

Dev: On   Prod: On

Configuring php.ini: Development vs Production

Here are the recommended php.ini settings for each environment:

Development Configuration

; php.ini — Development error_reporting = E_ALL display_errors = On display_startup_errors = On log_errors = On error_log = /var/log/php/error.log log_errors_max_len = 4096 html_errors = On report_memleaks = On

Production Configuration

; php.ini — Production error_reporting = E_ALL display_errors = Off display_startup_errors = Off log_errors = On error_log = /var/log/php/error.log log_errors_max_len = 4096 html_errors = Off report_memleaks = On
Panelica tip: Panelica lets you configure php.ini settings per user including error_reporting, display_errors, and log_errors through the panel GUI. Each user's PHP-FPM pool runs isolated, so one user's error settings never affect another's.

Setting the Error Log Location

The error_log directive specifies where PHP writes its error logs. You can set it to a file path or use syslog to integrate with your system's logging infrastructure.

; Log to a specific file error_log = /var/log/php/error.log ; Log to syslog error_log = syslog ; Per-site error log (in PHP-FPM pool config) php_admin_value[error_log] = /home/username/logs/php_error.log

Make sure the directory exists and is writable by the PHP-FPM process user. A common mistake is setting the error log to a path that does not exist or that the web server user cannot write to — PHP silently fails to log anything.

Runtime Error Configuration

While php.ini settings are the primary configuration method, you can also set error handling at runtime in your code. This is useful for temporary debugging or for application-specific error handling:

<?php // Set error reporting level at runtime error_reporting(E_ALL); // Toggle display errors ini_set('display_errors', '1'); // Development ini_set('display_errors', '0'); // Production // Set custom error log ini_set('error_log', '/path/to/custom/error.log'); // Manual error logging error_log("Something went wrong: user $userId not found"); // Log with specific destination type error_log("Critical error!", 1, "[email protected]"); // Email error_log("Debug info", 3, "/path/to/custom.log"); // Append to file
Warning: Some settings like display_startup_errors cannot be changed at runtime because startup errors occur before your script runs. These must be set in php.ini or the PHP-FPM pool configuration.

Custom Error Handlers

PHP lets you register custom functions to handle errors and exceptions. This is powerful for implementing structured logging, sending error notifications, or integrating with error tracking services like Sentry or Bugsnag.

set_error_handler()

This function registers a callback that intercepts all non-fatal errors (warnings, notices, deprecations). Fatal errors (E_ERROR, E_PARSE) cannot be caught by set_error_handler() — use register_shutdown_function() for those.

set_error_handler(function ($severity, $message, $file, $line) { $severityMap = [ E_WARNING => 'WARNING', E_NOTICE => 'NOTICE', E_DEPRECATED => 'DEPRECATED', E_STRICT => 'STRICT', ]; $level = $severityMap[$severity] ?? 'UNKNOWN'; $timestamp = date('Y-m-d H:i:s'); $logEntry = "[{$timestamp}] [{$level}] {$message} in {$file}:{$line}\n"; error_log($logEntry, 3, '/var/log/php/app_errors.log'); // Return false to also run PHP's built-in error handler // Return true to suppress PHP's built-in handler return true; });

set_exception_handler()

This catches any uncaught exception that bubbles up to the top level without being caught by a try/catch block:

set_exception_handler(function (Throwable $e) { $timestamp = date('Y-m-d H:i:s'); $class = get_class($e); $logEntry = sprintf( "[%s] [EXCEPTION] %s: %s in %s:%d\nStack trace:\n%s\n", $timestamp, $class, $e->getMessage(), $e->getFile(), $e->getLine(), $e->getTraceAsString() ); error_log($logEntry, 3, '/var/log/php/exceptions.log'); // Show user-friendly error page in production if (!ini_get('display_errors')) { http_response_code(500); include 'error_500.html'; } });

register_shutdown_function()

This is your last line of defense. It runs when the script terminates, including after fatal errors. Use it to catch the errors that set_error_handler() cannot:

register_shutdown_function(function () { $error = error_get_last(); if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR])) { $message = sprintf( "[FATAL] %s in %s:%d", $error['message'], $error['file'], $error['line'] ); error_log($message); // Send alert to monitoring system } });

Try/Catch/Finally: Structured Exception Handling

Modern PHP (7.0+) uses exceptions as the primary error handling mechanism. The try/catch/finally pattern gives you fine-grained control over how your application responds to different types of errors:

try { $pdo = new PDO($dsn, $user, $pass); $stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?"); $stmt->execute([$userId]); $user = $stmt->fetch(PDO::FETCH_ASSOC); if (!$user) { throw new \RuntimeException("User {$userId} not found"); } } catch (PDOException $e) { // Database-specific error handling error_log("Database error: " . $e->getMessage()); http_response_code(503); echo json_encode(['error' => 'Service temporarily unavailable']); } catch (\RuntimeException $e) { // Application logic error http_response_code(404); echo json_encode(['error' => $e->getMessage()]); } catch (\Throwable $e) { // Catch-all for any other error/exception error_log("Unexpected error: " . $e->getMessage()); http_response_code(500); echo json_encode(['error' => 'Internal server error']); } finally { // Always runs — cleanup resources $pdo = null; }
PHP 8.0+ tip: You can now use catch without a variable when you do not need the exception object: catch (SomeException) instead of catch (SomeException $e). This is useful when you just want to handle the error type without inspecting the details.

PHP Error Types: A Practical Guide

Here are the most common PHP errors you will encounter, what causes them, and how to fix them:

Undefined Variable / Undefined Array Key E_WARNING

Cause: Accessing a variable or array key that has not been set.

Fix: Use the null coalescing operator: $value = $array['key'] ?? 'default'; or isset() checks.

Cannot redeclare function E_ERROR

Cause: Including a file with function definitions multiple times.

Fix: Use require_once or include_once instead of require or include. Better yet, use autoloading with Composer.

Allowed memory size exhausted E_ERROR

Cause: Script uses more memory than memory_limit allows.

Fix: Increase memory_limit in php.ini or optimize code (use generators, process data in chunks, free unused variables with unset()).

Maximum execution time exceeded E_ERROR

Cause: Script runs longer than max_execution_time.

Fix: Increase the timeout for long-running tasks: set_time_limit(300); or optimize the code. For truly long tasks, use CLI scripts or background queues.

Setting Up Xdebug for Debugging

Xdebug is the essential PHP debugging extension. It provides step-by-step debugging, stack traces, profiling, and code coverage analysis. Here is how to set it up:

1
Install Xdebug — Use PECL or your system's package manager:
pecl install xdebug
2
Configure php.ini — Add Xdebug configuration:
zend_extension=xdebug xdebug.mode=debug,develop xdebug.start_with_request=trigger xdebug.client_host=127.0.0.1 xdebug.client_port=9003 xdebug.log=/var/log/php/xdebug.log
3
Configure your IDE — In VS Code, install the "PHP Debug" extension. In PhpStorm, Xdebug support is built in. Set breakpoints in your code and start a debug session.
Never enable Xdebug in production. It adds significant overhead (30-50% slower execution) and can expose sensitive debugging information. Use it only in development environments.

Log Rotation: Preventing Disk Full Disasters

PHP error logs can grow to gigabytes if left unchecked. Set up log rotation to keep them manageable:

# /etc/logrotate.d/php-errors /var/log/php/*.log { daily missingok rotate 14 compress delaycompress notifempty create 0640 www-data www-data postrotate systemctl reload php8.3-fpm 2>/dev/null || true endscript }

This configuration rotates logs daily, keeps 14 days of history, compresses old logs, and sends a reload signal to PHP-FPM so it starts writing to the new file.

Structured Logging with Monolog

For production applications, structured logging with Monolog (PSR-3 compatible) is far superior to raw error_log() calls. Monolog supports multiple output handlers, log levels, and formatters:

composer require monolog/monolog <?php use Monolog\Logger; use Monolog\Handler\StreamHandler; use Monolog\Handler\RotatingFileHandler; use Monolog\Formatter\JsonFormatter; $log = new Logger('app'); // Auto-rotating file handler (JSON formatted) $handler = new RotatingFileHandler('/var/log/php/app.log', 30, Logger::DEBUG); $handler->setFormatter(new JsonFormatter()); $log->pushHandler($handler); // Usage $log->info('User logged in', ['user_id' => 42, 'ip' => $_SERVER['REMOTE_ADDR']]); $log->warning('Slow query detected', ['query' => $sql, 'time_ms' => 1520]); $log->error('Payment failed', ['order_id' => 789, 'reason' => $e->getMessage()]);

JSON-formatted logs are searchable, parseable, and can be ingested by centralized logging systems like the ELK stack (Elasticsearch, Logstash, Kibana) or Grafana Loki.

Common PHP Errors and Fixes

Here is a quick-reference table of the errors developers encounter most frequently:

ErrorCauseFix
White screen, no errordisplay_errors=Off + no log_errorsEnable log_errors, check error log
Headers already sentOutput before header() callMove header() before any echo/HTML; check for BOM
Class not foundMissing autoload or requireRun composer dump-autoload
Permission deniedPHP process cannot write to pathFix file/directory ownership and permissions
Segmentation faultExtension conflict or PHP bugUpdate PHP, disable extensions one by one
Undefined constantTypo in constant name or missing defineUse defined() check, verify spelling

Error Handling Checklist

Use this checklist when setting up error handling for any PHP project:

  • error_reporting = E_ALL in both environments
  • display_errors = Off in production, On in development
  • log_errors = On in both environments
  • error_log points to a writable directory
  • Log rotation is configured (logrotate or Monolog's RotatingFileHandler)
  • Custom error handler registered with set_error_handler()
  • Custom exception handler registered with set_exception_handler()
  • Shutdown function catches fatal errors with register_shutdown_function()
  • User-friendly error pages displayed for 404 and 500 errors
  • Xdebug installed in development only (never production)
  • Sensitive data (passwords, API keys) never logged in plain text
  • Error monitoring/alerting configured for critical errors

Wrapping Up

PHP error handling is not glamorous, but it is essential infrastructure that separates amateur deployments from professional ones. The key principles are straightforward: always report all errors, never display them to users in production, always log them to files, use custom handlers for structured logging, and set up log rotation so your disk does not fill up.

The most common mistake is deploying with display_errors = On in production, which exposes your file paths, database connection details, and code structure to anyone who triggers an error. The second most common mistake is deploying with log_errors = Off, which means errors happen silently and you never know about them until users complain.

Get these fundamentals right, add structured logging with Monolog, set up Xdebug in your development environment, and you will be able to diagnose and fix PHP issues faster than you ever thought possible.

Share:
Atomic updates included.