Debug WordPress with WP_DEBUG Query Monitor and error logging

Debugging WordPress effectively requires three complementary tools working together: the built-in WP_DEBUG constant that exposes PHP errors and notices, a file-based error log so errors are captured without showing them to site visitors, and the Query Monitor plugin that profiles database queries, hooks, HTTP requests, and template loading in real time. Without this setup, silent PHP notices and poorly optimised queries can degrade site performance for weeks before anyone notices. WP_DEBUG is configured in wp-config.php via four constants: WP_DEBUG enables the debug mode, WP_DEBUG_LOG writes errors to wp-content/debug.log, WP_DEBUG_DISPLAY controls whether errors appear on screen (always set to false on production), and SCRIPT_DEBUG forces WordPress to load the unminified versions of core CSS and JavaScript files. The debug.log file accumulates errors silently on staging and production, letting you review them without exposing information to visitors. Query Monitor is a free plugin that adds a toolbar panel to every admin and front-end page showing every database query with its execution time, the hook or function that triggered it, slow queries highlighted in red, the complete hook waterfall, all HTTP API calls, and the loaded template hierarchy. It is invaluable for identifying which plugin is responsible for a slow query or an unexpected hook firing. For programmatic debugging you can use error_log() with print_r() or var_export() to write any value to debug.log without interrupting page output. The MySQL full-text search guide and the EXPLAIN guide explain how to act on the slow queries Query Monitor surfaces. Always disable WP_DEBUG and remove the debug log before going live — the log file can leak sensitive path information if publicly accessible.

Problem: PHP errors are silently swallowed, slow database queries go unnoticed, and there is no easy way to see which hooks and templates are firing on a given page.

Solution: Add the debug constants to wp-config.php and use error_log() to write any variable to debug.log for inspection:

// In wp-config.php — place ABOVE the "That's all" line

// Enable debug mode
define( 'WP_DEBUG', true );

// Log errors to wp-content/debug.log (never show them on screen)
define( 'WP_DEBUG_LOG',     true  );
define( 'WP_DEBUG_DISPLAY', false );

// Force WordPress to use unminified JS/CSS (helpful for JS debugging)
define( 'SCRIPT_DEBUG', true );

// Suppress errors on screen even if WP_DEBUG_DISPLAY slips through
@ini_set( 'display_errors', 0 );

// -------------------------------------------------------------------
// Helpers for manual debugging — add to functions.php temporarily

// Write any variable to debug.log
function ha_log( $label, $value ) {
    // phpcs:ignore WordPress.PHP.DevelopmentFunctions
    error_log( '[HA DEBUG] ' . $label . ': ' . print_r( $value, true ) );
}

// Usage examples
ha_log( 'post ID',      get_the_ID() );
ha_log( 'query vars',   $wp_query->query_vars );
ha_log( 'user object',  wp_get_current_user() );

// Tail the log from the command line:
// tail -f wp-content/debug.log

// -------------------------------------------------------------------
// Protect debug.log from public access — add to .htaccess
// <Files "debug.log">
//     Require all denied
// </Files>

NOTE: The debug.log file grows indefinitely — on an active staging server it can reach hundreds of megabytes within days. Truncate it regularly with truncate -s 0 wp-content/debug.log or add a weekly cron job to rotate it. Always add wp-content/debug.log to .gitignore so credentials or path information from error messages are never committed to version control. On production, if you need to capture a specific error without enabling global debug mode, use set_error_handler() scoped to the relevant code path instead of setting WP_DEBUG site-wide.