Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 041 / 100

Debugging WordPress: a practical guide for developers

Find problems quickly.

Expona Admin6 min read

Introduction

Production errors. Mysterious slowdowns. Plugin conflicts that vanish and reappear without warning. Every WordPress developer has faced moments when a site behaves in ways that make no sense—and the stakes only rise when client sites are involved. Debugging WordPress: a practical guide for developers means moving beyond guesswork and toward systematic troubleshooting that saves hours of investigation.

Whether you're tracking down a performance regression, isolating a plugin interaction, or understanding why a custom endpoint returns 500 errors, the right debugging tools cut through the noise. We'll walk through four proven techniques that let you find problems quickly—from local development to production environments—and show you how to combine them effectively.

WP_DEBUG — Enable with WP_DEBUG_LOG in development

The most fundamental lever in WordPress debugging is the WP_DEBUG constant. It's been available since WordPress 2.3, and for good reason: it catches PHP notices, warnings, and deprecated functions that would otherwise run silently in the background.

Open your wp-config.php and add or update these lines:

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

The three constants work together:

  • WP_DEBUG activates all notices and warnings.
  • WP_DEBUG_LOG writes debug output to /wp-content/debug.log instead of breaking the page.
  • WP_DEBUG_DISPLAY set to false prevents debug output from showing on the front end (which is almost always what you want in shared or production environments).

With this setup, every time a plugin calls an undefined variable or uses a deprecated hook, you'll have a timestamped record. Real-world scenario: you update a theme and suddenly widgets stop loading. Check debug.log and find a notice: Undefined property: CustomWidget::$args. Now you know exactly where to look.

Monitor the log actively during development:

php
tail -f wp-content/debug.log

This keeps the log visible in your terminal as errors arrive. You'll catch many issues—undefined array keys, type mismatches, missing sanitization—before they reach code review or production.

A word of caution: never leave WP_DEBUG enabled in production with WP_DEBUG_DISPLAY set to true. Debug output can expose file paths and sensitive information to visitors. If you must debug production, use WP_DEBUG_LOG only, and consider restricting who can see the log file.

Query Monitor — Queries, hooks, HTTP calls and errors per request

WP_DEBUG tells you something went wrong. Query Monitor tells you what happened before, during, and after that moment. This plugin is arguably the most powerful tool in a WordPress developer's arsenal.

Install it as you would any plugin, then open the floating panel at the top of any admin or front-end page. Query Monitor displays:

  • Database queries — every SQL statement, execution time, and the code that triggered it.
  • Hooks fired — all actions and filters, ordered chronologically, with their callbacks.
  • HTTP requests — outbound calls to APIs or other servers, with response codes and timing.
  • Errors and warnings — PHP notices, deprecations, and fatal errors in a dedicated panel.
  • Variables — global state at any given moment.

Let's say a page loads in 3 seconds when it should take 300ms. Open Query Monitor, click the "Queries" tab, and sort by execution time. Often you'll see a loop: 500 queries that fetch post meta one at a time instead of using a single meta query. You spot the culprit—a poorly written custom post type loop—and refactor to use get_posts() with meta_query instead.

Here's what that refactor might look like:

php
// Before: N+1 query problem
$posts = get_posts( array( 'post_type' => 'product' ) );
foreach ( $posts as $post ) {
    $price = get_post_meta( $post->ID, '_price', true ); // 1 query per post
}

// After: single query with meta
$posts = get_posts( array(
    'post_type' => 'product',
    'meta_query' => array(
        array(
            'key' => '_price',
            'compare' => 'EXISTS'
        )
    )
) );

Query Monitor also reveals hook chains. If you're hooking into save_post and something else is firing an unexpected custom endpoint, the "Hooks" tab shows the entire execution order. You can then conditionally disable your callback or reorder with priority adjustments.

The HTTP tab is invaluable when debugging third-party integrations. You see every API call your theme or plugins make, response status, response body, and latency. If a payment gateway integration is slow, check whether it's making extra calls you didn't expect.

Xdebug — Step through PHP code

Some problems demand line-by-line inspection. A function returns the wrong value. A condition isn't evaluating as expected. A variable gets corrupted between two lines. WP_DEBUG and Query Monitor show you the outcome, but Xdebug lets you pause execution and watch the state evolve.

Install Xdebug on your development machine (not production). On Linux with PHP-FPM, add to php.ini or a file in /etc/php/x.x/mods-available/:

php
zend_extension=xdebug.so
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=127.0.0.1
xdebug.client_port=9003
xdebug.log=/tmp/xdebug.log

Restart PHP. Then configure your IDE (VS Code, PHPStorm, etc.) to listen on port 9003. Most IDEs have a "Listen for Debug Connection" button. Once active, the next request will pause at a breakpoint.

Set a breakpoint in your code by clicking the line number in your editor. Load the page, and execution pauses there. Now you can:

  • Inspect variables and their types.
  • Evaluate expressions in the console.
  • Step forward one line at a time or jump to the next breakpoint.
  • Trace the call stack to understand how you reached this function.

Real example: a custom authentication function rejects valid tokens. Set a breakpoint inside the validation logic, step through the token parsing, inspect the payload, and discover that json_decode() is returning null because the token string has an extra space. Fix applied in 2 minutes instead of 20.

Xdebug is overkill for most daily work—Query Monitor handles 90% of cases faster. But when you're deep in a custom plugin with complex logic, Xdebug is irreplaceable.

Health check — Troubleshooting mode disables plugins for you only

Plugin conflicts are notorious in WordPress. A new plugin breaks something, but you don't know which one. Disabling all plugins site-wide affects users. Enter Health Check & Troubleshooting, a core team–recommended plugin that lets you enable troubleshooting mode.

Install Health Check from the plugin repository. Navigate to Tools → Site Health → Troubleshooting. Click "Enable Troubleshooting Mode." This disables all plugins and switches to a default theme—for you only. Logged-in users with admin privileges see a stripped-down site, but visitors see the normal site.

Now test: does the issue persist? If yes, it's a theme or WordPress core issue. If no, a plugin is responsible. Re-enable plugins one by one until the issue reappears. That's your culprit.

This approach saves enormous amounts of guesswork. We've used it to identify that a popular SEO plugin was loading JavaScript on every page that conflicted with a custom analytics implementation. Once identified, we either updated the plugin, configured it differently, or swapped it for an alternative.

The Health Check plugin also runs a suite of tests: PHP version compatibility, REST API availability, loopback requests, file permissions, etc. Review the results to spot misconfigurations before they become production issues.

Common mistakes

  • Leaving WP_DEBUG_DISPLAY on in production. This exposes file paths and internal state. Always set it to false if debugging remotely, and rotate it off entirely once done.
  • Not clearing Query Monitor's data between tests. If you're trying to measure the impact of a change, refresh the page and get a clean slate. Cached data from previous requests muddies the comparison.
  • Ignoring deprecation warnings. WordPress deprecates functions for a reason. Using wp_make_content_safe() when wp_kses_post() is the modern alternative creates technical debt. Fix deprecations as you encounter them.
  • Debugging production without proper protection. If you must enable WP_DEBUG on a live site, restrict debug.log access to admin IPs only, monitor it closely, and disable it the moment you've gathered information.
  • Assuming Query Monitor slows things down. It does add overhead, but only during development. Never leave it active on production.

Key takeaways

  • Enable WP_DEBUG and WP_DEBUG_LOG in all development environments. They catch mistakes early and cost nothing.
  • Query Monitor is your primary tool for performance investigation and hook tracing. Make it a habit to review its output after each code change.
  • Xdebug is powerful but reserved for complex logic debugging. Set it up once and use it when intuition fails.
  • Health Check's troubleshooting mode systematically isolates plugin conflicts without affecting site visitors.
  • Combine these tools rather than relying on a single one. WP_DEBUG finds the existence of errors, Query Monitor shows their impact, Xdebug reveals their cause, and Health Check narrows scope.
  • Always disable or secure debugging output before deploying to production.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles