Skip to content
System nominalUTC --:--:--Articles 080

The White Screen of Death, and How to Read It

A blank page is a fatal error with nowhere to print. Where the log lives on four host types, and how to bisect plugins without showing errors to visitors.

Martin Kiel · @mkiel

WordPress internals

Topic
WP
Section
WordPress Internals
Published
16 OCT 2026
Reading time
7 MIN
Words
1763W
Technical cover graphic for The White Screen of Death, and How to Read It
Generated schematic — WP series, REV 2.6.

The page arrives, the request takes eleven milliseconds, and the browser gets 2704 bytes of HTML that says "There has been a critical error on this website." No stack trace, no file name, no clue. That is the good version of the failure. The worse version is an empty response body and a 500 status, which is what people mean when they say white screen of death.

Both are the same event: PHP stopped in the middle of the request, the fatal error handler or the web server swallowed the details, and no log was configured in the place you are looking. The article you searched for is about the visual symptom. The fix is never visual. It is a file on disk that already contains the error, or a constant that makes PHP write one.

Everything below was produced on a local WordPress 7.1.3 install running PHP 8.5.6 behind the built-in PHP server, with a memory limit of 48M. That is not your stack, and the file paths differ. The behaviour of a fatal error does not.

Version note

WordPress 7.1.3 was the current release in the download directory when this was written, and PHP 8.5 was the current stable branch. If you are on a 6.x install, the recovery-mode error page and the fatal error handler described here still apply; they landed in 5.2. The wording of the page has barely changed since.

A fatal error has three places to hide

When PHP hits an unrecoverable error it prints a message if display_errors is on, and it writes the same message to the error log if log_errors is on. Production configurations turn the first off, which is correct and is also why you cannot see anything. So the message goes to one of three destinations, and "white screen" almost always means you are looking in the wrong one.

The web server error log is the first place. On a cPanel host that is usually ~/logs/error_log or the Error Log icon in the panel. On Plesk it is /var/www/vhosts/<domain>/logs/error_log. On a typical Ubuntu box with nginx it is /var/log/nginx/error.log, and with Apache either that vhost's error log or /var/log/apache2/error.log. On hosts where each site has its own pool, a per-site PHP log such as ~/logs/php_errorlog sits next to the access log. Ask the host for the path of "the PHP error log for this site" rather than a specific file name, because the file name is the one thing that varies.

The second place is wp-content/debug.log, and it only exists once you make WordPress create it. The third place is the connection: when the request dies before any of this, the server returns an empty body with a 500 status, and curl -I is how you learn that it was a 500 and not a redirect or a cache hit. On a host with a page cache in front of PHP, rule out a cache before you rule in a fatal: a cached copy of yesterday's response is served without executing a line of PHP, which is why the blank page appears on every URL except the ones the cache has never seen. Request a URL with a query string that cannot be cached and the failure usually reproduces on the first try.

WP_DEBUG without showing errors to visitors

The advice to "turn on WP_DEBUG" is right and incomplete. There are three constants, and only one of them is dangerous. Add this to wp-config.php, above the line that requires wp-settings.php:

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );

What each one does is specific. WP_DEBUG raises the error reporting level and, on current core, registers the fatal error handler that turns a fatal into the recovery-mode page. WP_DEBUG_LOG points PHP's error_log at wp-content/debug.log. WP_DEBUG_DISPLAY controls display_errors, which is what decides whether your visitors read the stack trace. Setting display to false while logging to true is the configuration you want on a live site: you get the file, nobody else gets the file path.

If WP_DEBUG is defined as true and WP_DEBUG_DISPLAY is left out, the default is false, so the middle line above is the one doing the work. I still write all three because the next person to open the file should not have to remember the default.

Debug log on a live site

wp-content/debug.log is inside the web root. If your host does not block .log files, the log that found your bug is also on the internet. Either configure WP_DEBUG_LOG as a path outside the document root or add a rule that denies access to it. I have seen both, and the second one gets forgotten.

Reading the log after you break a plugin on purpose

Here is the whole loop, run locally. I dropped one file into wp-content/mu-plugins/ that calls a function which does not exist:

<?php
// Reproduction: a plugin that calls a helper it assumed exists.
add_action( 'init', function () {
	wpsifero_missing_helper_function();
} );

With WP_DEBUG false and display_errors off, one request to the front page returns a 500 and nothing else. The web server log for the same request contains the decisive line:

[Fri Oct 16 15:14:57 2026] 127.0.0.1:51883 [500]: GET / - syntax error, unexpected token "}", expecting "," or ";" in /private/tmp/m-wperrors-lab/wordpress/wp-content/mu-plugins/zz-parse.php on line 5

Now flip WP_DEBUG and WP_DEBUG_LOG on, keep display off, and request the same page again. The browser gets the same generic page, and wp-content/debug.log gets this:

[16-Oct-2026 21:14:26 UTC] PHP Fatal error:  Uncaught Error: Call to undefined function wpsifero_missing_helper_function() in /private/tmp/m-wperrors-lab/wordpress/wp-content/mu-plugins/zz-broken.php:4
Stack trace:
#0 /private/tmp/m-wperrors-lab/wordpress/wp-includes/class-wp-hook.php(353): {closure:/private/tmp/m-wperrors-lab/wordpress/wp-content/mu-plugins/zz-broken.php:3}('')
#1 /private/tmp/m-wperrors-lab/wordpress/wp-includes/class-wp-hook.php(377): WP_Hook->apply_filters(NULL, Array)
#2 /private/tmp/m-wperrors-lab/wordpress/wp-includes/plugin.php(523): WP_Hook->do_action(Array)
#3 /private/tmp/m-wperrors-lab/wordpress/wp-settings.php(779): do_action('init')
#4 /private/tmp/m-wperrors-lab/wordpress/wp-load.php(50): require_once('/private/tmp/m-...')
#5 {main}
  thrown in /private/tmp/m-wperrors-lab/wordpress/wp-content/mu-plugins/zz-broken.php on line 4

The file and the line at the end are the answer, and the frames below tell you how execution got there. Read the last frame first. If the last frame is inside a plugin, that plugin is your suspect. If the last frame is inside a theme's functions.php, you have a theme problem. If the last frame is WordPress core, do not conclude that core is broken; you have a plugin calling something that does not exist, and core is where the failure surfaced.

Whether you get a blank page or a message depends on when the request dies

This part is worth knowing because it changes what you will find. Current WordPress registers a shutdown handler that catches fatal errors, so a plugin that fatals during init does not produce a white page. It produces the critical error page and an email to the admin address about recovery mode. A blank response needs the error to happen before that handler is registered, which is what a parse error in a plugin file does, or it needs the handler switched off.

I reproduced the genuinely empty case by parsing the same broken file with WP_DISABLE_FATAL_ERROR_HANDLER defined as true. The response is a 500 with a zero-length body, and the only record is the server log. That is the exact configuration people describe when they say the site went white and the host said nothing. If that is what you have, the answer is not in the site at all; it is in the log the host is holding.

So the first decision is not which plugin, it is which failure. If the site shows the critical error message, the failure happened after WordPress was most of the way up, and the log exists. If the response is genuinely empty, assume a parse error or a missing file and go straight to the server log or a file-system diff against the last deploy.

Bisect with the file system, not with the plugin screen

Once you have the log line, the fix is usually to disable the plugin it names. When the log is missing or useless, bisect. WP-CLI makes this one command, and it works even when the admin is unreachable:

wp plugin list --status=active --field=name > /tmp/plugins.txt
while read -r p; do wp plugin deactivate "$p"; done < /tmp/plugins.txt
wp plugin activate akismet

Deactivate everything, then activate in batches. The screen will not tell you anything the log does not, so the value of this approach is on hosts where you have shell access and no error log: half the set, request the page, then narrow. On a host with no WP-CLI, renaming a plugin directory over SFTP has the same effect, because WordPress deactivates a plugin whose directory has disappeared. Rename, do not delete. You want it back.

One caveat about the bisect: it changes the site. Deactivating a cache plugin can leave advanced-cache.php behind, and that file still runs. If the failure is a white screen and disabling plugins changed nothing, look at wp-content/mu-plugins/ and wp-content/advanced-cache.php as well. Must-use files are loaded before normal plugins, so a broken must-use file will defeat the entire exercise and keep the site down while every plugin in the list reads inactive.

The memory fatal looks exactly like this

There is one fatal error that routinely arrives dressed as a white screen, and the log line for it is completely different from the ones above. It reads:

PHP Fatal error:  Allowed memory size of 50331648 bytes exhausted (tried to allocate 90112 bytes) in /var/www/html/wp-includes/class-wpdb.php on line 2324

If the file in that line is class-wpdb.php, do not start by raising WP_MEMORY_LIMIT. The file name is where the allocation happened, not where the work came from, and in the next article in this series I walk through the plugin query that produced it. The short version is that the fault line tells you which request died, not which limit to change. Changing the limit turns a broken page into a slower broken page.

What to do on Monday

  1. Put the three debug constants in wp-config.php on staging, with display off and the log path inside the web root if and only if the host blocks .log. Then break a plugin on purpose and confirm the fatal lands in the file you expect. Finding out that the log is unwritable during an outage is a bad use of an outage.
  2. Ask the host for the exact path of the per-site PHP error log, and write it in the runbook. Four host types, four paths, no guessing at 2am.
  3. Record the plugin activation state, so that after an update you can diff it. The common white screen is a new plugin version parsing on a PHP version the host did not change.
  4. Leave WP_DEBUG_DISPLAY off on production, always. Logging is for you; display is for attackers looking for an absolute path.

Martin Kiel · @mkiel

WordPress internals

Writes about the load-bearing parts of WordPress: the options table, hook ordering, WP_Query behaviour and the database work that decides whether a plugin scales or falls over.

Continue

Related in WordPress Internals