Something is wrong and WordPress is not telling you what. A page is blank, a form silently fails, an admin screen half loads, or a plugin update has broken a layout with no message anywhere. WordPress hides PHP errors by default, which is right for a live site and useless when you need to fix one.
Debug mode is how you get the real message. It is a few lines added to a settings file called wp-config.php, it works on any host, and it turns “the site is broken” into a file path and a line number.
By the end of this you will have errors writing quietly to a log file visitors cannot see, you will know what the entries mean, and you will be able to read a stack trace and name the plugin responsible. A stack trace is the list PHP prints when it gives up, showing which piece of code called which, back to the start. It looks impenetrable and is not, and I go through a real one line by line further down.
You do not need to write any code. You paste four lines into one file, load the broken page, then read what turns up.
Before you start
You need one of the following to edit wp-config.php:
- SFTP or FTP access, meaning a program such as FileZilla that connects to your server’s files. SFTP is the encrypted one and the one to prefer.
- Your host’s File Manager (cPanel, Plesk, or whatever panel they give you). Easiest if you have never used FTP, since it runs in the browser.
- SSH, if you have it. That is a text-only command line on the server. Plenty of shared hosting has none, and nothing below needs it.
wp-config.php sits in the root of the install, the top-level folder alongside wp-admin and wp-content, usually called public_html. It holds your database password, so treat it carefully. Open it in a plain text editor such as Notepad, TextEdit or VS Code, never in Word, which adds formatting that breaks the file.
Download a copy before you change anything. This is the one genuinely risky moment in the job. A missing semicolon or a stray character before the opening <?php takes the whole site down, and putting the original back in ten seconds beats any amount of care. If the site is already down, start with fixing the white screen of death and come back once you can load a page.
Step 1: Add the debug constants to wp-config.php
Each line below defines a constant, a named setting WordPress reads once and cannot change afterwards. You are turning four switches on and off.
Open wp-config.php and find this line near the bottom:
/* That's all, stop editing! Happy publishing. */
Everything you add must go above that line. Below it, WordPress has already loaded and your constants are ignored.
Paste this in:
// Turn the error reporting engine on.
define( 'WP_DEBUG', true );
// Write everything to a log file instead of the browser.
define( 'WP_DEBUG_LOG', true );
// Never print errors into the page on a site the public can see.
define( 'WP_DEBUG_DISPLAY', false );
// Belt and braces: stop PHP itself echoing anything either.
@ini_set( 'display_errors', 0 );
Save, upload, and load the broken page. Nothing will look different, and that is the point. The errors are now going to a file.
If WP_DEBUG is already defined further up the file (some hosts and migration tools add it set to false), edit that line rather than adding a second. The first definition wins, so a duplicate does nothing.
What each constant actually does
These four get confused constantly, and the difference matters.
WP_DEBUG is the master switch. With it on, PHP reports everything including notices and deprecations, and WordPress reports its own complaints such as calling a function the wrong way. With it off, WordPress drops PHP back to reporting only the more serious errors and warnings. On its own it sends output wherever PHP is configured to send it, which on most hosts means straight onto the page.
WP_DEBUG_LOG redirects that output to a file. Set to true, WordPress writes to wp-content/debug.log. Set to a path, it writes where you point it. It does nothing unless WP_DEBUG is also true.
WP_DEBUG_DISPLAY controls whether errors are printed into the page HTML. The default is true, which is why debugging a live site sometimes fills the header with warnings and breaks the layout. Set it false and you get the log without the mess.
SCRIPT_DEBUG is unrelated to PHP. It loads the full versions of WordPress’s own JavaScript and CSS instead of the minified ones, which have had every space and line break stripped out to make them smaller, and are unreadable as a result. Use it when an error in your browser’s developer console points at a line inside wp-includes/js/dist/:
// Load unminified core JS and CSS. Front-end debugging only.
define( 'SCRIPT_DEBUG', true );
It makes pages heavier, so turn it off afterwards.
One more worth knowing. If WP_ENVIRONMENT_TYPE is set to development, WordPress switches debugging on when you have not defined WP_DEBUG yourself, which is one reason a development copy shows notices the live site never does. The local value is a separate one and does not do this, despite what the name suggests, so do not assume a local install is reporting anything until you have checked.
Step 2: Move the log somewhere the public cannot read it
By default the log lands at wp-content/debug.log, and on most hosts anyone who guesses the URL can read it. Stack traces contain server paths, plugin internals and sometimes fragments of database queries: free reconnaissance for anyone poking at your site.
The clean fix is to write the log outside the web root, the folder the public can reach through a browser. Anything above it sits on the same server with no URL at all, so nobody can fetch it. Point WP_DEBUG_LOG at an absolute path, meaning the full location from the top of the server rather than one relative to WordPress:
// Absolute path, outside public_html, in a directory PHP can write to.
define( 'WP_DEBUG_LOG', '/home/yoursite/logs/wp-errors.log' );
Two conditions: the directory must already exist, and PHP must be able to write to it. If the log never appears, that is almost always why. Most hosts have a logs folder next to public_html that qualifies.
If you cannot write outside the web root, block the file instead. Apache and nginx are the two web servers almost every host runs, and only Apache reads .htaccess. If you do not know which you are on, ask your host; cPanel hosting is usually Apache. On Apache, put this in wp-content/.htaccess, creating the file if there is none. The leading dot makes it hidden, so your File Manager may need “show hidden files” turned on:
<Files debug.log>
Require all denied
</Files>
On nginx, in the server block (you will normally need to ask your host to add this):
location ~* /wp-content/debug\.log$ {
deny all;
}
Either way, delete the log when you have finished. It grows without limit and nothing rotates it for you.
Step 3: Reproduce the problem, then read the log
An empty log is not a clean bill of health. It only records what happens while it is on, so go and do the thing that breaks: load the page, submit the form, save the post, run the import. Then open the log.
A typical fatal error entry on PHP 8 looks like this:
[14-Nov-2024 09:14:22 UTC] PHP Fatal error: Uncaught TypeError: count(): Argument #1 ($value) must be of type Countable|array, null given in /home/yoursite/public_html/wp-content/plugins/example-slider/includes/class-slides.php:212
Stack trace:
#0 /home/yoursite/public_html/wp-content/plugins/example-slider/includes/class-slides.php(88): Example_Slides->render_items(NULL)
#1 /home/yoursite/public_html/wp-includes/class-wp-hook.php(324): Example_Slides->output('')
#2 /home/yoursite/public_html/wp-includes/plugin.php(517): WP_Hook->do_action(Array)
#3 /home/yoursite/public_html/wp-content/themes/example-theme/footer.php(23): do_action('wp_footer')
#4 {main}
That is a lot of text, and almost all of it is scenery. Read it from the top. The first line says what went wrong and exactly where: class-slides.php line 212, in a plugin folder called example-slider. That folder name is the plugin’s slug, the short name WordPress files it under, so you know which plugin to deactivate.
Each numbered line below is one frame, meaning one step in the chain of code that led to the crash. They read bottom to top in the order things happened. #3 is where the chain started, in the theme’s footer. #2 and #1 are core running a hook, which is its way of letting plugins join in at a set moment. #0 is the plugin calling its own method. Core frames are noise. The useful ones have wp-content/ in the path, and the deepest of those is nearly always the culprit.
The trap is the frame naming your theme. A theme calling do_action( 'wp_footer' ) is doing what every theme does. Blame the code that threw, not the code that happened to be running when it threw.
Notices, warnings and fatal errors are not the same problem
A log full of entries does not mean a log full of bugs. The severity in the message tells you how much to care.
Fatal error stops execution dead. Whatever was meant to render after that point does not. This is the one behind blank pages and half-loaded admin screens. Fix these first, always.
Warning means PHP carried on but something was wrong. Undefined array key, Attempt to read property on null, a failed file read. These often explain a missing image or an empty section while the rest of the page is fine.
Deprecated means the code works today and will stop working on a future PHP version. This is by far the most common thing filling logs on PHP 8.1 and up, mostly because older plugin code passes null where a string is expected. A maintenance signal, not an emergency, and chasing it while the site is on fire wastes your afternoon.
Notice is the mildest and usually means plugin code is sloppy rather than broken.
PHP 8 moved the goalposts, which catches people out. Several things that were warnings on PHP 7 are now fatal, passing the wrong type into a function being the big one. A plugin that “worked fine last week” and broke the day the host bumped PHP has not changed. The rules around it did.
The host’s PHP error log is a different file
debug.log only records errors that happen after WordPress has started loading. If PHP dies before that, there is nothing to write and no file to write it to.
That covers a real set of problems: a typo in wp-config.php, a broken .user.ini (a small file some hosts use to set PHP options per site), a mangled must-use plugin, a missing PHP extension, or the process being killed for using too much memory. A must-use plugin, or mu-plugin, lives in wp-content/mu-plugins and runs automatically with no way to deactivate it from the dashboard. In each case debug.log stays empty and the site stays broken.
The host-level log is where those land. Look in your hosting panel under Errors, Logs, or Error Log. If there is no such screen, ask support for your site’s PHP error log and tell them when the problem happened. With SSH, error_log files often sit in the site root or a sibling logs directory.
My rule: if the WordPress log is empty but the site is broken, go straight to the host log. If both are empty, the failure is happening before PHP runs, which points at the web server or .htaccess rather than WordPress.
Query Monitor, for when the page loads but is wrong
Logs are the right tool when something crashes. When the page renders but is slow, shows the wrong posts, or fires a request it should not, a log tells you very little.
Query Monitor is the tool for that case. Install it like any other plugin and a new menu appears in the black admin bar. It shows every database query with its timing and the code that triggered it, every hook fired, every request the site made out to another server, and any PHP errors from that page load, each attributed to the plugin that caused it.
It is a development tool, so install it, find what you need, then remove it. If you are digging because pages are slow, the query panel is normally where the answer is, and the same place I start on speed work.
How to check it worked
Force an error deliberately so you know logging is wired up before you trust an empty file.
Add this to your theme’s functions.php, load any page on the site, then remove it. That file is under Appearance, Theme File Editor in the dashboard, though editing there is unforgiving, so copy the original first:
// Temporary. Writes one line to the debug log to prove logging works.
error_log( 'ABCode logging test at ' . current_time( 'mysql' ) );
Open the log. If your test line is there, an empty log genuinely means no errors are being raised. If it is not, the path is wrong or not writable, and anything you conclude from a quiet log is worthless.
A caveat on functions.php: a theme update overwrites it. For anything you intend to keep, use a child theme or a small site-specific plugin. Diagnostic one-liners you remove five minutes later are fine as they are.
When it does not work
The debug.log file never appears
Three usual causes. WP_DEBUG is false or missing, so WP_DEBUG_LOG has nothing to do. The constants sit below the “stop editing” line. Or the target directory is not writable by PHP, which applies to wp-content as much as a custom path.
Errors still print on the page
Something is overriding WP_DEBUG_DISPLAY. A plugin, an mu-plugin, or the host’s PHP configuration can all set display_errors back on. The @ini_set( 'display_errors', 0 ) line in Step 1 covers the host’s own setting, because wp-config.php runs before anything else does. It cannot stop a plugin turning it back on afterwards, since plugins load later. Some managed hosts control it centrally, so ask them.
The log is enormous and unreadable
Almost always one deprecation notice repeating on every page load. Delete the file, load the broken page once, and read the fresh log. One reproduction beats thousands of identical warnings.
The error names a file in wp-includes or wp-admin
Core files rarely break on their own. Look further down the trace for the first frame inside wp-content. If every frame really is core, suspect a partially uploaded core file and re-upload WordPress, leaving wp-content and wp-config.php alone.
The fatal error stops the log being written
If PHP runs out of memory, the process can be killed before it flushes anything useful. Raise the limit with define( 'WP_MEMORY_LIMIT', '256M' ); and check the host’s error log for an “Allowed memory size exhausted” line, which names the file that was allocating.
Common questions
Is it safe to enable WP_DEBUG on a live site?
Yes, as long as WP_DEBUG_DISPLAY is false and the log is not publicly readable. That combination is invisible to visitors. What is not safe is WP_DEBUG alone with display on, which shows server paths to anyone loading the page while an error fires.
Where is the WordPress error log?
wp-content/debug.log by default, once WP_DEBUG and WP_DEBUG_LOG are both on. Before that it does not exist. WordPress logs nothing until you tell it to.
Should I leave debug mode on permanently?
No. The log grows without limit and nothing rotates it, so on a busy site it can eventually eat your disk quota. Turn it on to investigate, turn it off and delete the file afterwards.
What is the difference between debug.log and the host error log?
debug.log covers errors during WordPress execution and is the more readable of the two. The host log catches everything, including failures before WordPress loads. Check WordPress first, then the host log if WordPress has nothing.
Do I need to reset the constants to false, or can I delete them?
Either works. Deleting the block returns you to the defaults, which is WP_DEBUG off. I leave the lines in place set to false so they are quick to flip next time.
Turning it off, and what to do with what you found
Set WP_DEBUG back to false and delete the log file. The site returns to hiding errors from visitors, which is where a live site should be. Note what you found first, because the log is the only record and you are binning it.
Most of the time the trace points at one plugin and the fix is obvious: update it, replace it, or report the bug to whoever wrote it. Sometimes it points at a plugin nobody has touched in years that is now incompatible with the PHP version your host has moved to, and the honest answer is replacement rather than patching. Where the functionality is specific to how you work, a small custom plugin is often cheaper than bending something generic into shape.
If the trace is unfamiliar and the site is live, that is what WordPress fixes and repairs is for. Send me the log entry rather than a description of the symptom. A stack trace tells me in thirty seconds what a screenshot cannot in an hour.