Skip to content

Capture ALL uncaught exceptions as exceptions#1462

Open
dave-newson wants to merge 1 commit intoelastic:mainfrom
dave-newson:native-throwable-capture
Open

Capture ALL uncaught exceptions as exceptions#1462
dave-newson wants to merge 1 commit intoelastic:mainfrom
dave-newson:native-throwable-capture

Conversation

@dave-newson
Copy link
Copy Markdown

This PR fixes an issue in the native uncaught exception handling, where only the \Exception class was eligible for decomposition into a verbose exception capture, leaving all other Throwables to capture as flat errors.

This change now matches the FQCN of lastThrown back to the Uncaught <FQCN>: in an effort to capture ALL exception types, and the specifically relevant exception.

What was happening?

We have some pre-framework code super early which throws new \RuntimeException(, and APM was seeing this as a flat error rather than an Exception with a trace, unlike everything else we have going into APM.

This was traced back to the explicit Uncaught Exception: prefix test not matching what PHP emits: Uncaught RuntimeException:

In APM this becomes an Error with the full stack-trace in the Message, and N/A for the origin function, with no Exception detailing. In turn, this representation really messes with Error Grouping and causes a lot of duplication in APM.

Why match the FQCN?

lastThrown isn't diligently unset, there's some opportunity for it to be populated when various event handlers get fired. The FQCN comparison is intended to ensure the lastThrown is definitely the same Exception mentioned in the native "Uncaught" message.

FQCN matching seems to work for even namespaced throwables: https://3v4l.org/lrIIP#veol

namespace Hello\World;
class Derp extends \LogicException {}

$x = new \Hello\World\Derp('hello');
echo get_class($x);
throw $x;

Hello\World\Derp
Fatal error: Uncaught Hello\World\Derp: hello in /in/lrIIP:7

Why hasn't anyone else seen this?

I'd wager that if you're using APM you're probably also using some kind of Framework with its own error-capture, and you'll then be using a library to augment APM with more telemetry, and this goes through a lot of non-php-native-paths to register the Exception with APM (eg. ElasticApm::createErrorFromThrowable), and code almost never lets an Exception escape to the native PHP handler.

Only when a non-\Exception type is thrown and truly uncaught, does this kick in.

Oh god the test case

I didn't want to disrupt the existing uncaught exception test case, so the test case you see added is just a duplication of the existing Uncaught Exception test. In all honesty this should probably become a dataProvider thing, but I didn't want to make it painful to validate the fix. Better to add the fix and then refactor the test cases.

@cla-checker-service
Copy link
Copy Markdown

cla-checker-service bot commented Apr 10, 2026

💚 CLA has been signed

@github-actions
Copy link
Copy Markdown

🤖 GitHub comments

Just comment with:

  • run docs-build : Re-trigger the docs validation. (use unformatted text in the comment!)

… all Uncaught throwables are accurately captured if they emit to the native handler.
@dave-newson dave-newson force-pushed the native-throwable-capture branch from 5474690 to 392c2ba Compare April 10, 2026 04:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant