Error messages are useful to you and useful to attackers. A message such as “Access denied for user ‘root’@’localhost'” or a file path tells them how your site is built. On a live site, visitors must never see technical errors.
The two environments
| Development (your computer) | Production (the live site) | |
|---|---|---|
display_errors | On | Off |
log_errors | On | On |
error_reporting | E_ALL | E_ALL |
| What users see | The detailed error | A friendly page |
Notice that error_reporting stays E_ALL in both. You still want to record everything. You only stop showing it.
Set it up
; production php.ini
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/app-errors.log
error_reporting = E_ALL
PHP ships php.ini-production and php.ini-development as starting points.
A friendly error page
<?php
set_exception_handler(function (Throwable $e): void {
error_log((string) $e); // full detail goes to the log
http_response_code(500);
header("Content-Type: text/html; charset=utf-8");
echo "<h1>Something went wrong</h1>";
echo "<p>We have been told and are working on it. Please try again later.</p>";
});
Notice the status code 500. Search engines and monitoring tools rely on it. A friendly page that returns 200 OK hides the failure.
Show detail only in development
<?php
$isDev = getenv("APP_ENV") === "development";
ini_set("display_errors", $isDev ? "1" : "0");
Read the mode from an environment variable (see Environment Variables), so the same code works everywhere.
Never leak these
- File paths and line numbers
- SQL queries and database names
- Stack traces
- Library versions
- Configuration values
<?php
function publicMessage(Throwable $e, bool $debug): string
{
return $debug ? $e->getMessage() : "Something went wrong. Please try again later.";
}
$e = new RuntimeException("SQLSTATE[HY000] [1045] Access denied for user 'root'");
echo publicMessage($e, false) . "\n";
echo publicMessage($e, true);
Something went wrong. Please try again later.
SQLSTATE[HY000] [1045] Access denied for user 'root'
User mistakes are different
A validation error (“Please enter a valid email”) is not a server error. Show it to the user clearly. Only unexpected failures (database down, bug) get the generic message.
Test the failure path
It is easy to build a site that works when everything is fine. Break something on purpose in a test environment (wrong database password, missing file) and check what a visitor would see.
Monitor
A log nobody reads does not help. Send errors to a service or at least check the log regularly, so you hear about problems before your visitors tell you.
Debug mode flags in other tools
- WordPress:
WP_DEBUGshould befalseon a live site, andWP_DEBUG_DISPLAYmust befalseif you log withWP_DEBUG_LOG. - Laravel:
APP_DEBUG=falsein production.
Never leave phpinfo() on a live site
phpinfo() prints your whole configuration, paths and extensions. Delete test pages after you use them.