Error Reporting in Production
Error Reporting in Production Why Production Error Handling Is Different In simple terms: During development, you want to see every error immediately, with full details. On a live website, the same me
Error Reporting in Production
Why Production Error Handling Is Different
In simple terms: During development, you want to see every error immediately, with full details. On a live website, the same messages are dangerous: they can reveal file paths, database names, SQL queries, passwords in stack traces, and the software versions you use — a roadmap for attackers. In production, errors must be hidden from visitors but recorded for developers.
Technically: PHP's behavior is controlled by php.ini directives (display_errors, log_errors, error_reporting, error_log, and others), plus the application's own error handlers and framework settings. PHP ships with two templates — php.ini-development and php.ini-production — whose recommended values differ exactly in these areas. Error disclosure is classified as a security weakness (information exposure) because it helps attackers plan further attacks.
What Error Messages Can Reveal
| Leaked Detail | Example | Helps Attackers |
|---|---|---|
| File system paths | /var/www/shop/src/Payment/Gateway.php | Map the application; target path traversal |
| SQL queries and table names | SQLSTATE[42S22]: Unknown column ... in 'users' | Craft SQL injection payloads |
| Stack traces with arguments | PDO->__construct('mysql:host=...', 'admin', ...) | Obtain credentials |
| Versions | PHP/8.2.3, framework version in error pages | Find known vulnerabilities |
| Configuration | Debug pages listing environment variables | Steal API keys and secrets |
Recommended Production Settings
; php.ini (production)
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/app-error.log
error_reporting = E_ALL
html_errors = Off
zend.exception_ignore_args = On
expose_php = Off
| Directive | Production | Development | Meaning |
|---|---|---|---|
display_errors | Off | On | Show errors in the output |
display_startup_errors | Off | On | Show errors that occur while PHP starts |
log_errors | On | On | Write errors to the log |
error_log | A protected file path (or unset to use stderr in containers) | Optional | Where errors are written |
error_reporting | E_ALL | E_ALL | Which errors are reported — always all of them |
zend.exception_ignore_args | On | Off | Omit function arguments from stack traces (they may contain secrets) |
expose_php | Off | Either | Remove the X-Powered-By: PHP/x.y.z header (see Hiding PHP) |
error_reporting stays at E_ALL in production: errors must still be reported — to the log — even though they are not displayed. Lowering it hides problems from developers, not from attackers.
Verifying the Configuration
Different environments often use different php.ini files: the command line, PHP-FPM, and Apache's module each have their own. Verify the settings in the environment that serves web requests, for example with a protected admin-only script:
<?php
// admin/config-check.php — protect with authentication or run via CLI with the FPM ini file
$expected = [
'display_errors' => false,
'display_startup_errors' => false,
'log_errors' => true,
'expose_php' => false,
'zend.exception_ignore_args' => true,
];
foreach ($expected as $setting => $shouldBeOn) {
$isOn = filter_var(ini_get($setting), FILTER_VALIDATE_BOOL);
printf("%-28s %-6s %s\n", $setting, $isOn ? 'On' : 'Off', $isOn === $shouldBeOn ? 'OK' : 'CHANGE');
}
printf("%-28s %-6s %s\n", 'error_reporting', error_reporting() === E_ALL ? 'E_ALL' : (string) error_reporting(), error_reporting() === E_ALL ? 'OK' : 'CHANGE');
Output (on a correctly configured production server)
display_errors Off OK
display_startup_errors Off OK
log_errors On OK
expose_php Off OK
zend.exception_ignore_args On OK
error_reporting E_ALL OK
ini_get() returns strings such as "1", "0", "On", or ""; FILTER_VALIDATE_BOOL interprets all of them correctly. On the command line, php --ini shows which configuration files are loaded, and php -i | grep display_errors shows the CLI's values (which may differ from PHP-FPM's).
Warning: Never leave
phpinfo()scripts on a production server. They display the complete configuration, environment variables, and paths to anyone who finds them.
Showing Users a Safe Error Page
With display_errors = Off, an uncaught error results in a blank page or a generic server error. Provide a proper page instead:
- Application level: a global exception handler and shutdown function log the error and render a friendly page with HTTP status
500(see Custom Error Handler). - Web server level: configure static error pages for failures PHP cannot handle, such as PHP-FPM being down (
502/504):
error_page 500 502 503 504 /errors/500.html;
location = /errors/500.html {
internal;
}
A good error page apologizes, offers a way forward (home page, support contact), and may show a reference ID that also appears in the log entry — so support staff can find the exact error without exposing details:
$reference = bin2hex(random_bytes(4)); // e.g. "9f3c1a7e"
error_log("[ref $reference] " . $exception);
// show: "Something went wrong. Reference: 9f3c1a7e"
Framework Debug Modes
Frameworks have their own debug switches, which must be off in production:
| Framework / Tool | Production Setting |
|---|---|
| Laravel | APP_ENV=production, APP_DEBUG=false |
| Symfony | APP_ENV=prod, APP_DEBUG=0 |
| Debug toolbars and error pages (Whoops, Symfony Profiler, Laravel Debugbar, Telescope) | Not installed or disabled in production |
A detailed framework error page in production can expose the full environment, including database passwords and API keys. Deployment scripts should verify these settings.
Other Information Leaks
| Leak | Prevention |
|---|---|
| Directory listings | Disable automatic indexes (autoindex off; in Nginx, Options -Indexes in Apache) |
Backup and editor files (config.php.bak, .swp, .env.old) | Never create them in the document root; block dotfiles and backup extensions |
Version control folders (.git/) | Deploy without them, or keep them outside the document root |
| Verbose API errors | Return generic messages with error codes; log details server-side |
| Health-check endpoints | Report "OK"/"degraded" without internal details, or protect them |
Monitoring Errors
Hidden errors must still reach developers quickly:
- Centralized logging: ship logs from all servers to one place (for example, an ELK stack, Grafana Loki, or a cloud logging service).
- Error tracking services such as Sentry or Bugsnag group identical errors, record context, and notify the team.
- Alerts on error spikes, repeated
500responses, or fatal errors. - Log hygiene: rotate logs, restrict access, and avoid logging passwords, tokens, or personal data (see Error Logging).
Common Mistakes
display_errors = On in Production
Effect: Visitors (and attackers) see internal details.
Fix: Turn it off in the production php.ini, and verify it in the web context.
Lowering error_reporting Instead of Hiding Display
Effect: Real problems are never logged.
Fix: Keep E_ALL; control visibility with display_errors.
Framework Debug Mode Left On
Effect: Environment variables and source code exposed.
Fix: Set production environment variables and check them during deployment.
No Monitoring
Effect: Errors go unnoticed until users complain.
Fix: Collect logs centrally and alert on errors.
Best Practices
- Use
php.ini-productionas the basis for production servers. - Hide errors from users, log everything, and keep
error_reporting = E_ALL. - Enable
zend.exception_ignore_argsand disableexpose_phpin production. - Show friendly error pages with reference IDs, at both the application and web server levels.
- Disable debug modes, remove
phpinfo()files, block sensitive files, and monitor logs actively.
Official reference: https://www.php.net/manual/en/security.errors.php