Technote

Development workflow Practical

Debugging without var_dump: logs and tools, in order

Dumping to the screen leaks internals on a live site, corrupts AJAX and REST responses, and shows nothing at all across a redirect. The three tools that replace it have a natural order.

var_dump() is easy to learn and answers immediately, which is why the hand reaches for it first. The problem is that the range in which it works is narrower than it feels — and when it does not work, that fact is itself silent.

Where dumping to the screen falls apart

First, on a live site the visitors see it. A dump carries paths, table names, configuration values and occasionally fragments of credentials, and all of it renders straight onto the page. Believing you removed every trace before deploying and leaving one line behind is not a rare event.

Second, it corrupts responses that are not HTML. Put a dump inside an AJAX handler or a REST route and the JSON arrives with prose bolted to the front, parsing fails, and the screen shows an unhelpful generic error. You have added a second symptom on top of the bug you were chasing.

Third, it shows nothing across a redirect. In code that saves and immediately navigates away — form handlers, for instance — the page changes before the output reaches anyone. Seeing nothing reads as “this code never runs”, when it is running perfectly well.

Fourth, it triggers headers-already-sent warnings. Any code that sets a cookie or a header after output has begun now fails too, layering a different class of error over the original.

Step one: write to a log, not the screen

The baseline tool is the WordPress debug log, and the essential part is the combination: recording on, display off.

// wp-config.php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );      // writes to wp-content/debug.log
define( 'WP_DEBUG_DISPLAY', false ); // never render to the page
@ini_set( 'display_errors', 0 );

The third line is the whole point of this article. Enabling WP_DEBUG alone sends errors to the browser, so whenever you turn logging on, turn display off in the same breath. Confirm the log file itself is not fetchable over the web too — block it in the server configuration, or move the log path outside the document root.

From there, use error_log() at the points you care about. Two habits make logs worth having: tag them, and write the context alongside.

error_log( sprintf(
    '[acme][order] sync %s status=%s user=%d',
    $order_id,
    $status,
    get_current_user_id()
) );

// arrays and objects go through a serialiser first
error_log( '[acme][order] payload ' . wp_json_encode( $payload ) );

Tags let you filter a large log down to the lines you want. Context — the order number, the user, the state — lets you work out the reproduction conditions from the log alone. A line reading “got here” is useful only in the moment it was written.

The order of tools — logs when you want a value, a debugger when you want the path

Step two: observation tools, development only

Sometimes the question is not a value but what ran, and how much of it: which queries fired and how often, what is attached to a given hook, which file rendered the template. That is what an observation plugin such as Query Monitor is for.

These tools belong on development and staging only. They expose query and hook internals and add measurement overhead to every request. Leaving one enabled in production and forgetting is a genuinely common accident, so keeping a per-environment list of active plugins is the safer arrangement.

Checking the debugging setup — the bottom two have each caused real incidents

Step three: when the problem is control flow, not a value

Some bugs resist logging entirely, because the question is which path was taken. When a branch goes the way you did not expect, when a value changes somewhere inside a filter chain, or when the answer depends on which callback ran first, the code grows more complicated faster than you can add log lines to it.

That is the moment for a step debugger. Set a breakpoint, walk forward a line at a time and read the call stack, and what you were inferring from thirty log lines is simply visible. The setup costs a little time, and one difficult bug usually repays it.

Finally, do not leave debugging code behind. Temporary logging and scratch variables come out once the cause is found. Skimming the changed files before committing is enough, or put a simple check for stray error_log calls into CI.

Production logging and deployment are covered in the From local to production series, and query counts and timings continue in Profiling WordPress performance. Related writing sits in the development workflow archive, and to have logging, performance and security configuration sorted out in one pass on a running site, see our optimization program.

More on this topic

All technotes

Development workflow Intro

Decide the question before you open the dashboard

A dashboard opened without a question produces observations, not decisions. Writing one sentence before you open it changes the whole meeting.

Product planners 7 min read

Development workflow Intro

The real problem behind “just add this feature”

A request has already been translated once. The person asking turned their need into a solution before they spoke — and building that solution usually solves the wrong…

Product planners 7 min read

Development workflow Intro

Scope a redesign by writing down what you will not do

A redesign with no completion condition never completes. "While we are at it" is how schedules die, and the document that stops it is not a to-do list…

Product planners 7 min read

₩270,000 · Join the program