How can I get PHP to produce a backtrace upon errors?

Viewed 64547

Trying to debug PHP using its default current-line-only error messages is horrible. How can I get PHP to produce a backtrace (stack trace) when errors are produced?

12 Answers

My script for installing an error handler that produces a backtrace:

<?php
function process_error_backtrace($errno, $errstr, $errfile, $errline, $errcontext) {
    if(!(error_reporting() & $errno))
        return;
    switch($errno) {
    case E_WARNING      :
    case E_USER_WARNING :
    case E_STRICT       :
    case E_NOTICE       :
    case E_USER_NOTICE  :
        $type = 'warning';
        $fatal = false;
        break;
    default             :
        $type = 'fatal error';
        $fatal = true;
        break;
    }
    $trace = array_reverse(debug_backtrace());
    array_pop($trace);
    if(php_sapi_name() == 'cli') {
        echo 'Backtrace from ' . $type . ' \'' . $errstr . '\' at ' . $errfile . ' ' . $errline . ':' . "\n";
        foreach($trace as $item)
            echo '  ' . (isset($item['file']) ? $item['file'] : '<unknown file>') . ' ' . (isset($item['line']) ? $item['line'] : '<unknown line>') . ' calling ' . $item['function'] . '()' . "\n";
    } else {
        echo '<p class="error_backtrace">' . "\n";
        echo '  Backtrace from ' . $type . ' \'' . $errstr . '\' at ' . $errfile . ' ' . $errline . ':' . "\n";
        echo '  <ol>' . "\n";
        foreach($trace as $item)
            echo '    <li>' . (isset($item['file']) ? $item['file'] : '<unknown file>') . ' ' . (isset($item['line']) ? $item['line'] : '<unknown line>') . ' calling ' . $item['function'] . '()</li>' . "\n";
        echo '  </ol>' . "\n";
        echo '</p>' . "\n";
    }
    if(ini_get('log_errors')) {
        $items = array();
        foreach($trace as $item)
            $items[] = (isset($item['file']) ? $item['file'] : '<unknown file>') . ' ' . (isset($item['line']) ? $item['line'] : '<unknown line>') . ' calling ' . $item['function'] . '()';
        $message = 'Backtrace from ' . $type . ' \'' . $errstr . '\' at ' . $errfile . ' ' . $errline . ': ' . join(' | ', $items);
        error_log($message);
    }
    if($fatal)
        exit(1);
}

set_error_handler('process_error_backtrace');
?>

Caveat: it is powerless to affect various 'PHP Fatal Errors', since Zend in their wisdom decided that these would ignore set_error_handler(). So you still get useless final-location-only errors with those.

Xdebug prints a backtrace table on errors, and you don't have to write any PHP code to implement it.

Downside is you have to install it as a PHP extension.

As php debug extensions, there is Xdebug and PHP DBG. Each one has its advantages and disadvantages.

If you can't install a debugger then use this function sorrounding the fatal error to get the "fatal stack". Check the code and example below that explains better how to use it:

// Give an extra parameter to the filename
// to save multiple log files
function _fatalog_($extra = false)
{
    static $last_extra;

    // CHANGE THIS TO: A writeable filepath in your system...
    $filepath = '/var/www/html/sites/default/files/fatal-'.($extra === false ? $last_extra : $extra).'.log';

    if ($extra===false) {
        unlink($filepath);
    } else {
        // we write a log file with the debug info
        file_put_contents($filepath, json_encode(debug_backtrace()));
        // saving last extra parameter for future unlink... if possible...
        $last_extra = $extra;
    }
}

Here's an example of how to use it:

// A function which will produce a fatal error
function fatal_example()
{
    _fatalog_(time()); // writing the log
    $some_fatal_code = array()/3; // fatality!
    _fatalog_(); // if we get here then delete last file log
}

Finally to read the contents of the log...

var_dump(json_decode(file_get_contents('/path/to-the-fatal.log')));

Hope that helps!

You can use (new \Exception())->getTraceAsString() instead of debug_backtrace():

set_error_handler(function(int $severity, string $message, string $file, int $line) {
    if (!(error_reporting() & $severity)) {
        return false;
    }

    $severities = [
        E_ERROR => 'ERROR',
        E_WARNING => 'WARNING',
        E_PARSE => 'PARSE',
        E_NOTICE => 'NOTICE',
        E_CORE_ERROR => 'CORE_ERROR',
        E_CORE_WARNING => 'CORE_WARNING',
        E_COMPILE_ERROR => 'COMPILE_ERROR',
        E_COMPILE_WARNING => 'COMPILE_WARNING',
        E_USER_ERROR => 'USER_ERROR',
        E_USER_WARNING => 'USER_WARNING',
        E_USER_NOTICE => 'USER_NOTICE',
        E_STRICT => 'STRICT',
        E_RECOVERABLE_ERROR => 'RECOVERABLE_ERROR',
        E_DEPRECATED => 'DEPRECATED',
        E_USER_DEPRECATED => 'USER_DEPRECATED',
    ];

    $e = new \Exception;

    // Just remove the current point from the trace
    $reflection = new \ReflectionProperty(get_class($e), 'trace');
    $reflection->setAccessible(true);
    $trace = $reflection->getValue($e);
    array_shift($trace);
    $reflection->setValue($e, $trace);

    $text = '';
    $text .= ($severities[$severity] ?? $severity) . ': ';
    $text .= "$message in $file($line)\n";
    $text .= "Stack trace:\n";
    $text .= $e->getTraceAsString();

    error_log($text);

    if (in_array($severity, [
        E_CORE_ERROR,
        E_ERROR,
        E_RECOVERABLE_ERROR,
        E_PARSE,
        E_COMPILE_ERROR,
        E_USER_ERROR,
    ], true)) {
        http_response_code(500);
        exit(255);
    }

    return true;
});

Output:

[16-Feb-2022 00:01:09 UTC] DEPRECATED: trim(): Passing null to parameter #1 ($string) of type string is deprecated in /home/user/project/doSomething.php(3)
Stack trace:
#0 /home/user/project/doSomething.php(3): trim(NULL)
#1 /home/user/project/index.php(4): doSomething()
#2 {main}
Related