An error names the line where it was raised, not the line that caused it. The engine’s own vocabulary says so. The PHP manual (read 2026-08-24) describes the fourth argument passed to a custom error handler, errline, as “the line number where the error was raised”, and documents Exception::getLine() as returning “the line in which the exception was created”. Raised and created are not the same event as caused. When the reported line looks obviously fine, it usually is. It is simply the first place where a bad value, a bad moment, or a bad name could no longer be tolerated.
Raised, created, caused
Three separate things happen, often in three separate files.
Caused is where the wrong decision was made: a function returned nothing instead of an object, a setting was read before it was written, a file was loaded twice.
Raised is where the engine could not continue. PHP does not scan your program for mistakes. It runs it, and it stops at the first instruction that cannot be executed with the values it has been handed. That instruction is what gets a line number.
Reported is what you read, which is the raise site plus, if you are lucky, a stack trace describing the calls that led there. The PHP manual’s wording for Throwable::getLine() is precise about this: it gets “the line on which the object was instantiated”. The exception object is a record made at the moment of failure. It is not an investigation.
The practical consequence is a rule you can apply without thinking: if the reported line contains a variable you did not set on that line, the bug is probably not on that line. Look at what is being read there, and go find where it was written.
The three shapes this takes
A value arrived wrong from somewhere else
The most common case, and the least interesting once you see it. Your code calls a method on something, and that something is not an object. The engine raises at the call, because the call is the first operation that cannot be performed. The mistake was made wherever that variable got its value: a lookup that found nothing and returned null, a query that matched no rows, an optional argument nobody passed, a configuration key that does not exist in this environment.
Nothing about the raise site is wrong. The line calling the method is often the most correct line in the file. It is only the first line that could not survive the value it was given, which is why reading it over and over teaches you nothing.
This is the same failure that produces the harder version of the problem, where nothing raises at all and the program simply returns a wrong answer. If your bug is that shape rather than this one, green tests and broken behavior is the situation you are actually in, and no line number will be offered to you at all.
A hook fired at the wrong moment
This one is specific to code that runs inside a framework or a CMS, and it is where “obviously fine” code produces error entries that name files you have never opened.
Your code is correct. Its timing is not. WordPress ships a function whose entire purpose is to report this class of mistake: _doing_it_wrong(), documented in the WordPress developer reference (read 2026-08-24) as marking “something as being incorrectly called”, and which only triggers its notice when WP_DEBUG is true. The notice that lands in your log names the core function that noticed the problem. It does not name the code that caused it.
The clearest public example is the translation notice introduced in WordPress 6.7 and tracked as WordPress Trac ticket #62175 (read 2026-08-24). The notice names the core function _load_textdomain_just_in_time and reports that translation loading for a given text domain was triggered too early. Every file path in that entry can be a core path. The cause is a plugin or a theme asking for translated strings before the point in the load order where WordPress is prepared to supply them. The fix is not in the file that was named. It is in the moment your code chose to run.
When you suspect timing, stop reading the line and start asking a different question: what has already happened by the time this runs, and what has not happened yet? Errors of this shape move when you move the hook, and they are unmoved by anything you do to the reported line.
A name was already taken
The third shape is a fatal that fires on a declaration rather than on a call. Two definitions with the same name become reachable in a single request, whether through an autoloader that resolves the same class from two places, a file included twice by two different paths, or a library vendored once by you and once by a dependency.
The engine raises on the second declaration, because the second is the one that cannot be completed. The second declaration is almost never the guilty one. What went wrong is that both became reachable at all, and the answer lives in your include and autoload configuration, not in either file that got named. The reported line here is the least informative line in the whole request: it tells you a collision happened and nothing at all about who arranged it.
Searching backward from the raise site
The reported line is a starting point, so use it as one.
- Read the trace bottom to top, not top to bottom. The top frame is the raise site you already know about. The frames beneath it are the chain of calls that produced the conditions. The frame you want is usually the first one that belongs to code you own.
- Find where the value came from, not where it was used. Search for assignments to that variable, for the function that returned it, and for the boundary it crossed to reach this scope. Boundaries are where values go wrong: a request parameter, a database result, a filter, a JSON payload, a config file.
- Ask when, not just what. If the code is correct on its own, the bug is timing or ordering. Check what the code depends on having already happened.
- Check what changed. A line that has worked for a year and fails today was not the change. A dependency update, a PHP version bump, a new plugin, or a new environment variable was.
- Reproduce with the smallest input that still fails. Cutting the input down usually names the cause faster than reading the code does.
An assistant is genuinely useful for step 1 and actively misleading at step 2. Pasted an error and a file, it will confidently rewrite the raise site to tolerate the bad value, which suppresses the message and preserves the bug. That is a direct consequence of it only being shown the raise site, and it is why what you give an assistant to look at decides the quality of what it tells you. The general discipline of checking rather than trusting applies with unusual force here, because a plausible fix to the wrong line looks exactly like a correct fix until the next request.
There is one more common source of distance between raise and cause: a change large enough that you never read all of it. When the guilty line lives in a diff you skimmed, reviewing a pull request that touches dozens of files is the problem to solve first, because no amount of log reading recovers context you never had.
Two things not to do
Do not silence the raise site. Wrapping the reported line in a null check, a try block, or a suppression operator makes the message stop. It does not make the value correct. The program then continues with data it should not have, and the next failure appears somewhere with no connection to the original cause. Handle a value defensively only when the wrong value is genuinely expected, never as a way to end a debugging session.
Do not edit core, and do not edit a dependency in place. If the trail leads into WordPress core, a framework’s own files, or something under vendor/, that is a sign you have found where the problem surfaced, not where it belongs. The next update overwrites the edit. Put the fix in your own code, in a child theme, in a plugin of your own, or in a patch reported to whoever maintains that code, and bring in a developer if the ownership is unclear.
FAQ
Is the reported line ever simply wrong? Occasionally, in the narrow sense: parse errors and errors inside compiled or generated code can point at a line that does not correspond neatly to what you wrote, which is why template engines and code generators have long-standing reports about line numbers. Far more often the line is correct and the expectation is wrong. It is reporting a raise, and you are reading it as a cause.
How do I tell a raise site from a cause without a stack trace? Look at what the line consumes. If everything the line touches was assigned on that same line, the bug is likely there. If it reads a variable, a property, or a return value produced elsewhere, it is a raise site, and the cause is wherever that value was produced.
The error names a file inside a plugin I did not write. Is that plugin at fault? Not necessarily. Code fails on the input it is given, and third-party code is often given bad input by yours, by another extension, or by a stale value in the database. Establish who called it before deciding who is responsible.
Why does the same bug report different lines on different runs? Because the raise site depends on the path the request took. Different input, different configuration, or a different order of loading can mean a different instruction is the first one that cannot proceed. A cause that moves its symptom around is usually shared state or load order rather than the code at any of the reported lines.


1 comment