Debugging

Turn on WordPress's debug constants, send errors to a log instead of the page, and read them without breaking production.

Syntaxdefine( '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 — sets error_reporting( E_ALL ) and enables core's _deprecated_function() and _doing_it_wrong() notices. Everything below depends on it being true.
  • WP_DEBUG_LOG — true is shorthand for the path WP_CONTENT_DIR . '/debug.log'. Giving it a string path is the longhand form of the same thing: both end up setting PHP's error_log ini value, so plain error_log() calls land in the same file either way.
  • WP_DEBUG_DISPLAY — false sets display_errors to off. null is not the same as false: it tells WordPress to leave the ini value alone entirely.
  • SCRIPT_DEBUG — enqueues the unminified core JavaScript and CSS instead of the .min builds, so a block editor stack trace points at readable code.
  • SAVEQUERIES — records every query in $wpdb->queries as [ 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

Example · php
<?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.

Example · php
<?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.

Example · php
<?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.

Example · php
<?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

  • Be the first to comment on this lesson.