Fatal Errors Finally Get a Stack Trace: PHP 8.5's fatal_error_backtraces
PHP 8.5 gives fatal errors a full stack trace by default. Here is how the fatal_error_backtraces directive works and how to tune it in production.
Every PHP developer has hit this wall. Production logs a fatal error, you read the message, and it tells you a line number in a file that is called from a dozen places. “Allowed memory size exhausted in Helper.php on line 42.” Great. Which of the twenty callers actually blew the budget? Historically the answer was to add temporary logging, redeploy, and wait for it to happen again. PHP 8.5 quietly removes that entire ritual by attaching a full stack trace to fatal errors, and it does so by default.
The gap PHP 7 left behind
PHP 7.0 was a turning point for error handling. It converted a large class of old-style errors into Error exceptions, which meant type errors, calls to undefined methods, and similar failures could be caught, logged with a trace, and handled gracefully. Combined with set_error_handler for warnings and notices, most day-to-day error conditions became observable.
But a handful of conditions were left out because they are genuinely unrecoverable. Exhausting the memory limit, exceeding max_execution_time, or stack overflow cannot safely continue execution, so PHP prints a message and exits. Until now, that message carried the file and line where the failure surfaced, but never the chain of calls that led there. For a memory exhaustion error deep inside a shared helper, the line number is often the least useful part of the picture.
What PHP 8.5 changes
Consider this deliberately small reproduction:
<?php
ini_set('memory_limit', '2M');
function my_heavy_function(): void
{
$str = str_repeat('A', 1024 * 1024 * 5);
}
my_heavy_function();
On PHP 8.4 and earlier, you get a single line with no context:
Fatal error: Allowed memory size of 2097152 bytes exhausted
(tried to allocate 5242912 bytes) in script.php on line 7
On PHP 8.5, the same error arrives with the trace attached:
Fatal error: Allowed memory size of 2097152 bytes exhausted
(tried to allocate 5242912 bytes) in script.php on line 7
Stack trace:
#0 script.php(7): str_repeat('AAAAAAAAAA...', 5242880)
#1 script.php(10): my_heavy_function()
#2 {main}
That second block is the whole point. You can now see that str_repeat was the culprit, that it was called from my_heavy_function, and that the function was invoked from the top level of the script. In a real application, that chain is what turns a ten-minute guessing game into a five-second read.
The new fatal_error_backtraces directive
The behavior is controlled by a new INI directive, fatal_error_backtraces, and it ships enabled. You do not need to change anything to benefit from it. If for some reason you want the old terse output back, you turn it off explicitly:
fatal_error_backtraces = Off
I have not found a good reason to disable it, but the toggle exists for environments that parse fatal error output with brittle tooling and would rather not adapt to the extra lines.
The feature came in through the “Error Backtraces v2” RFC, which passed 19 to 1. That lopsided vote reflects how uncontroversial it is. There is no new syntax to learn, no API to adopt, and the RFC notes no backward compatibility break beyond the changed text of the fatal error output itself.
How it plays with your existing error settings
This is where it matters for anyone running PHP in production, because the new trace respects the controls you already rely on.
The most important interaction is with display_errors. On a production server you should have display_errors = Off so that internal details never reach the browser. That setting still holds. With display off, neither the message nor the new backtrace is rendered to the response. Instead, when log_errors is on, the fatal error and its stack trace are written to your error log, which is exactly where you want them. So the mental model is simple: the backtrace shows up wherever your fatal errors already show up, and nowhere else.
Two more directives shape what the trace reveals. If you use zend.exception_ignore_args = On, the backtrace omits the arguments passed to each frame, which trims noise and avoids logging large or sensitive values. And the #[\SensitiveParameter] attribute, which has redacted arguments in exception traces since PHP 8.2, applies here too. A parameter marked sensitive will not have its value printed in the fatal error trace:
<?php
function connect(string $dsn, #[\SensitiveParameter] string $password): void
{
// If a fatal error unwinds through this frame,
// $password is shown as [redacted] in the trace,
// while $dsn is printed normally.
}
If you have been diligent about tagging credentials and tokens with #[\SensitiveParameter], that discipline now pays off in one more place. If you have not, this is a good nudge to start.
Putting it to work
For most teams the practical upgrade path is: move to PHP 8.5, confirm log_errors is on and pointed at a log you actually watch, and keep display_errors off in production. That is the whole setup. The next time a scheduled job at PHP Architect quietly dies from memory exhaustion, the log will tell you which report builder and which caller were on the stack, rather than just naming the shared helper where the allocation happened.
It also improves the local development story. With display_errors on in your dev environment, a fatal error now prints its trace straight to the terminal or the response, so you can diagnose a runaway recursion or an accidental multi-gigabyte allocation without reaching for Xdebug or scattering debug_backtrace() calls through the code.
A small caution worth stating plainly: this feature makes fatal errors more informative, not less frequent. It will not catch a memory exhaustion for you or let you recover from one, because these conditions remain unrecoverable by design. What it changes is how quickly you understand them after the fact. That is a modest promise, and PHP 8.5 keeps it well.
Wrapping up
Not every language feature needs to be flashy to earn its place. fatal_error_backtraces is a small change that removes a recurring source of wasted time, respects the security posture you already have, and asks nothing of your code. If you are planning a move to PHP 8.5, this is one of the features you will appreciate the first time production throws a fatal error and the log already contains the answer.