Debugging
Turn on WordPress's debug constants, send errors to a log instead of the page, and read them without breaking production.
define( 'WP_DEBUG', true ); define( 'WP_DEBUG_LOG', '/path/outside/webroot/debug.log' );WordPress hides PHP notices by default. With WP_DEBUG off, wp_debug_mode() narrows error_reporting() to fatals, warnings and parse errors, so deprecated calls and undefined-index notices in your own code never surface. The debug constants belong in wp-config.php, above the require_once ABSPATH . 'wp-settings.php'; line, because wp_debug_mode() runs early in the load sequence.
The constants
WP_DEBUG— setserror_reporting( E_ALL )and enables core's_deprecated_function()and_doing_it_wrong()notices. Everything below depends on it being true.WP_DEBUG_LOG—trueis shorthand for the pathWP_CONTENT_DIR . '/debug.log'. Giving it a string path is the longhand form of the same thing: both end up setting PHP'serror_logini value, so plainerror_log()calls land in the same file either way.WP_DEBUG_DISPLAY—falsesetsdisplay_errorsto off.nullis not the same asfalse: it tells WordPress to leave the ini value alone entirely.SCRIPT_DEBUG— enqueues the unminified core JavaScript and CSS instead of the.minbuilds, so a block editor stack trace points at readable code.SAVEQUERIES— records every query in$wpdb->queriesas[ sql, seconds, calling functions, start time, custom data ]. Expensive; development only.
Reading the output
Query Monitor, a plugin, renders the same material — queries, hooks, HTTP calls, PHP notices, rewrite matches — in an admin bar panel, which beats tailing a file for anything request-shaped. For cron, REST and WP-CLI work the log file is still the only place errors reliably appear.
Example
<?php
// wp-config.php — these must appear ABOVE the line that requires wp-settings.php.
// Report every PHP notice, warning and error, and switch on WordPress's own
// _deprecated_function() / _doing_it_wrong() notices.
define( 'WP_DEBUG', true );
// true is shorthand for WP_CONTENT_DIR . '/debug.log'.
// A string path is the longhand of the same setting — prefer it, because
// wp-content/debug.log is fetchable over HTTP on most hosts.
define( 'WP_DEBUG_LOG', __DIR__ . '/../logs/debug.log' );
// Never render errors into the response on a shared or public site: output
// emitted before headers are sent breaks redirects, cookies and JSON.
// Note that false and null differ — null leaves php.ini's display_errors alone.
define( 'WP_DEBUG_DISPLAY', false );
// Load wp-includes/js/dist/*.js rather than *.min.js.
define( 'SCRIPT_DEBUG', true );
// Populate $wpdb->queries. Each entry is:
// [ 0 => SQL, 1 => seconds, 2 => comma-separated calling functions,
// 3 => start timestamp, 4 => custom query data ]
define( 'SAVEQUERIES', true );When to use it
- A REST endpoint returns a 500 with an empty body and you need to see the actual fatal.
- A page prints "headers already sent" and you must find which plugin echoed early.
- An archive template takes four seconds and you want to know which code issued the queries.
- A block breaks in the editor and the minified core bundle gives an unreadable stack trace.
More examples
Debug settings driven by the environment
One wp-config.php that behaves correctly on every machine. Reach for this when the same repository is deployed to local, staging and production and you are tired of a developer accidentally committing WP_DEBUG true. The comment about wp_get_environment_type() matters: that function exists only after wp-settings.php has loaded, so config-time decisions must use getenv() or the constant directly.
<?php
// wp-config.php. wp_get_environment_type() is NOT available here — wp-settings.php
// has not run yet and no WordPress function is loaded — so read the raw value.
// ABSPATH is also usually defined further down this file, hence __DIR__.
$acme_env = getenv( 'WP_ENVIRONMENT_TYPE' ) ?: 'production';
define( 'WP_ENVIRONMENT_TYPE', $acme_env );
if ( 'production' === $acme_env ) {
define( 'WP_DEBUG', false );
} else {
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', __DIR__ . '/../logs/debug.log' );
define( 'WP_DEBUG_DISPLAY', false ); // still false: notices corrupt JSON and redirects
define( 'SCRIPT_DEBUG', true );
}A logging helper that respects the debug switch
Shows why WP_DEBUG_LOG is useful beyond errors: it repoints PHP's error_log destination, so ordinary error_log() calls land in the same file. Use this when you need to trace something that happens during a REST request, a cron event or a WP-CLI run, where var_dump has nowhere to go.
<?php
/**
* Write a labelled value to whatever WP_DEBUG_LOG pointed error_log at.
* Silent when debugging is off, so calls can stay in the code.
*/
function acme_log( string $label, $value = null ): void {
if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
return;
}
$message = $label;
if ( null !== $value ) {
// print_r's second argument returns the string instead of echoing it —
// echoing here would inject debug output into the page.
$message .= ' ' . ( is_scalar( $value ) ? (string) $value : print_r( $value, true ) );
}
error_log( $message );
}
add_action( 'wp_insert_post', function ( $post_id, $post, $update ) {
acme_log( $update ? 'updated post' : 'created post', array( 'id' => $post_id, 'type' => $post->post_type ) );
}, 10, 3 );Logging slow queries with SAVEQUERIES
The third element of each $wpdb->queries entry is the chain of calling functions, which is what turns "this page runs 400 queries" into "this plugin runs 400 queries". Use it when Query Monitor is unavailable — on a cron run, a REST call, or a CLI command, where there is no admin bar to look at.
<?php
// Drop in a mu-plugin on a development or staging box only.
add_action( 'shutdown', function (): void {
if ( ! defined( 'SAVEQUERIES' ) || ! SAVEQUERIES ) {
return;
}
global $wpdb;
foreach ( $wpdb->queries as $query ) {
// [0] SQL, [1] seconds, [2] the call stack that produced it.
if ( $query[1] > 0.05 ) {
error_log( sprintf( "SLOW %.4fs %s\n via %s", $query[1], $query[0], $query[2] ) );
}
}
error_log( sprintf( '%d queries on %s', $wpdb->num_queries, $_SERVER['REQUEST_URI'] ?? 'cli' ) );
} );
Discussion