A PHP 8.5 booking site shows a blank white page in production and nothing appears in its error log — how do you track down the cause?
answer
- status code first: 500 or 200
- the serving SAPI's settings, not the CLI's
- where error_log really points
- handlers that swallow without logging
- fatals: error_get_last() in shutdown
basics
~20 sCheck the status code, then the serving process's error settings and where its log really goes. Look for handlers that swallow failures, and capture fatal errors — which bypass set_error_handler — with a shutdown function reading error_get_last().
solid answer
~50 sStart with the response status. A `500` with an empty body is PHP's own signature for a fatal error when `display_errors` is Off and nothing had been output yet; a `200` with an empty body points at application code — an exception handler that rendered nothing, or an error handler returning `true` without logging. Next, check the settings of the process that serves the site, not the CLI's: `log_errors` must be On, `error_reporting` must not have been lowered (a stray `error_reporting(0)` does it), and `error_log` must point somewhere real — when it is unset or the file cannot be opened, PHP writes to the SAPI's logger instead, which under PHP-FPM usually means the web server's error log with a `PHP message:` prefix. Fatal errors such as exhausting `memory_limit` never reach `set_error_handler()`; a shutdown function reading `error_get_last()` can record them, and in PHP 8.5 that array can carry a `trace` key.
code
php · 12 lines<?php
register_shutdown_function(static function (): void {
$error = error_get_last();
$fatal = E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR;
if ($error === null || !($error['type'] & $fatal)) {
return;
}
$line = sprintf('[booking] fatal %d: %s in %s:%d', $error['type'], $error['message'], $error['file'], $error['line']);
error_log($line . PHP_EOL, 3, '/var/log/booking/php-fatal.log');
});go deeper
Recall that production hides errors from the page, so the answer is in a log, and that the status code tells you whether PHP hit a fatal error.
Explain how error_log decides the destination, including the fallback to the SAPI logger, and why the CLI's configuration can differ from the web process's.
Work the diagnosis in order — status, effective settings, log destination, swallowing handlers, fatal capture — and harden handlers so they log and set 500 before rendering.
Own an error-visibility standard: one log destination per environment, alerting on the fallback logs, and handlers reviewed so no failure can end silently.
## Step 1: read the status code A blank page means two very different things depending on the status the browser received. Check it in the browser's network panel or with a command-line HTTP client before touching any code. | Status and body | What usually happened | |---|---| | `500`, empty body | A fatal error with `display_errors` Off. PHP sets 500 itself when a fatal error occurs, display is off, no output has been sent and the status is still 200 | | `200`, empty body | PHP did not see a fatal error. An exception handler or error handler ran and produced nothing, or the page legitimately rendered nothing | | `502` or `504` | The PHP process died or timed out before answering — look at the process manager and web server, not at PHP's error settings | ## Step 2: check the settings of the serving process The CLI and the web SAPI can load different configuration, so `php -i` on the server proves nothing about the booking site. Inspect the values the web request actually runs with — for example a temporary, access-restricted script that logs `ini_get()` for the directives below. - `log_errors` must be `On` (its built-in default is Off). - `error_reporting` must include the levels you expect; a call to `error_reporting(0)` in a bootstrap file or a vendor script silences everything after it. - `display_errors` should stay `Off` in production; turn it on only in a staging copy when reproducing. ## Step 3: find where the log really goes `error_log` decides the destination, and each case has its own way of looking empty: 1. **Unset**: messages go to the SAPI's own logger. Under PHP-FPM they usually appear in the **web server's error log**, prefixed `PHP message:`, not in any application log. 2. **A file PHP cannot open** (wrong permissions, missing directory): PHP falls back to the SAPI's logger, so the configured file stays empty. 3. **`syslog`**: messages are in the system journal, not in a file. ## Step 4: look for handlers that swallow Application code can absorb failures before PHP's logging ever sees them: - a `set_error_handler()` callback that returns `true` without logging marks every warning handled; - a `set_exception_handler()` callback that renders an empty template and does not log. Because the handler **handles** the throwable, PHP raises no fatal error — so nothing is logged by PHP and the status stays **200** unless the handler sets it; - a broad `catch (\Throwable)` around the front controller that returns an empty response. ## Step 5: capture fatal errors explicitly Fatal errors — exhausting `memory_limit`, exceeding `max_execution_time`, a parse error in an included file — are not throwables and never reach `set_error_handler()` or `set_exception_handler()`. PHP logs them itself when `log_errors` is On. To route them to your own alerting as well, register a shutdown function that reads `error_get_last()` and checks for a fatal `type`. PHP 8.5 makes this easier: the new `fatal_error_backtraces` directive, **On by default**, adds a backtrace to fatal error messages, and `error_get_last()` gains a `trace` key for fatal errors when a backtrace is available (not for uncaught exceptions). ## Step 6: harden so it cannot recur - Ship an explicit production ini: display off, logging on, a known `error_log`. - Make every custom handler **log before it renders**, and set status 500 when headers are still unsent. - Alert on the web server's log as well as the application's, since fallback messages land there. - Keep `@` out of bootstrap code; before PHP 8.0 it could hide even fatal errors, and it still hides everything else. ## A worked example On the booking site, the network panel shows `500` with an empty body — a fatal error. The configured `error_log` file is empty, but the process cannot write to its directory after a deploy changed ownership, so PHP fell back to the SAPI's logger. The web server's error log holds the line: `PHP message: PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate ...)`. 134217728 bytes is the `memory_limit = 128M` that php.ini-production ships, and with PHP 8.5's `fatal_error_backtraces` On the message can carry a backtrace, here pointing at the availability search that loads every room for a year into one array. Two fixes follow: repair the log directory's ownership, and page through the availability query instead of loading it whole.
- Why does the status stay 200 when an exception handler renders an empty page?A handler registered with `set_exception_handler()` handles the uncaught throwable, so PHP never raises the `E_ERROR` that would otherwise set status 500 and write the `Uncaught ...` log line. Unless the handler calls `http_response_code(500)` and logs the throwable, the client gets an empty 200 and the log stays silent.
- The php.ini you edited shows log_errors = On, yet the site still logs nothing. What do you suspect?That the web SAPI is not reading that file, or a later setting overrides it. The CLI and the web SAPI can load different configuration, and application code can call `error_reporting(0)` or change directives at run time. Check the values from inside a web request, not from `php -i` on the command line.
saying these in an interview costs you the question
- Turn display_errors on in production to see the error quickly
- An empty 200 response means PHP hit a fatal error
- php -i on the server shows the settings the website uses
- set_error_handler() will catch memory_limit fatal errors for logging
- If the configured error_log file is empty, PHP logged nothing anywhere