[PHP-DEV] [DISCUSSION] User-land Throwable

Hello PHP Internals,

I would like to propose a discussion regarding two current limitations in PHP’s exception handling system that I believe could be addressed to improve flexibility and developer experience.

A few years ago I found that a library printed error traces wrong.
After a little research I found that there was a mix of 3rd party integration error + raised error around the current bridge implementation (http client).

There were several PHP applications with microservices architecture which I had access to (docker + sources).

So having the message and traces I’d like to have an error chain as it can be done chaining several errors through a new Exception(previous: $e).
But PHP does not allow you to manually implement Throwable. Instead you should extend Exception. But after that you still cannot override the getTrace() method because it’s final.

So my proposal is pretty simple: Remove both restrictions.

  1. Allow user classes to implement Throwable interface directly

User classes cannot implement the Throwable interface now.

class MyCustomThrowable implements Throwable {}

// Fatal error: Class MyCustomThrowable cannot implement interface Throwable, extend Exception or Error instead
  1. Make getTrace() non-final or provide alternative customization mechanism

Exception traces cannot be overridden now.

class MyCustomThrowable extends Exception {
function getTrace() { return []; }
}

try { throw new MyCustomThrowable(); }
catch (Exception $e) { var_dump($e->getTrace()); }

// Fatal error: Cannot override final method Exception::getTrace()

There are some points about the feature:

  • Microservice support: Preserve traces across service boundaries
  • Proxy/decorator patterns: Maintain original error context through wrappers
  • Unified error handling: Any object implementing Throwable can be thrown consistently
  • Testing improvements: Create mock throwables for unit tests
  • Performance optimization: Avoid deep call stacks in generated traces

What do you think about it? Are there disadvantages or points to have the exceptions in the current state?

···

Best regards,

Dmitrii Derepko.
@xepozz

Hi

Am 2025-07-28 11:41, schrieb Dmitry Derepko:

A few years ago I found that a library printed error traces wrong.
After a little research I found that there was a mix of 3rd party
integration error + raised error around the current bridge implementation
(http client).

There were several PHP applications with microservices architecture which I
had access to (docker + sources).

So having the message and traces I'd like to have an error chain as it can
be done chaining several errors through a new Exception(previous: $e).
But PHP does not allow you to manually implement Throwable. Instead you
should extend Exception. But after that you still cannot override the
getTrace() method because it's final.

So my proposal is pretty simple: Remove both restrictions.

I'm afraid I don't quite understand what actual goal you intend to solve with the proposal. The description of your use case is very abstract, can you provide a real-world example of a use-case you want to enable?

Best regards
Tim Düsterhus

On Mon, 28 Jul 2025 at 13:56, Tim Düsterhus <tim@bastelstu.be> wrote:

Hi

Am 2025-07-28 11:41, schrieb Dmitry Derepko:

A few years ago I found that a library printed error traces wrong.
After a little research I found that there was a mix of 3rd party
integration error + raised error around the current bridge
implementation
(http client).

There were several PHP applications with microservices architecture
which I
had access to (docker + sources).

So having the message and traces I’d like to have an error chain as it
can
be done chaining several errors through a new Exception(previous: $e).
But PHP does not allow you to manually implement Throwable. Instead you
should extend Exception. But after that you still cannot override the
getTrace() method because it’s final.

So my proposal is pretty simple: Remove both restrictions.

I’m afraid I don’t quite understand what actual goal you intend to solve
with the proposal. The description of your use case is very abstract,
can you provide a real-world example of a use-case you want to enable?

Best regards
Tim Düsterhus

| real-world example of a use-case you want to enable

Say I am implementing a job runner, I do its error handling, and I want to enrich the caught exception with additional helpful data for debugging. In this process I create a custom exception MyJobHandlerException, but the trace of the new exception includes my handler code, which the end user does not care about. I wish to overwrite the trace of the new MyJobHandlerException instance with the trace from the originally caught exception, but I cannot.

I was always sure that the reason behind all methods of \Exception being final is Larry-Garfield-blogpost length/depth so I never bothered to ask and made workarounds.

Hi

Am 2025-07-28 13:48, schrieb Rokas Šleinius:

Say I am implementing a job runner, I do its error handling, and I want to
enrich the caught exception with additional helpful data for debugging. In
this process I create a custom exception MyJobHandlerException, but the
trace of the new exception includes my handler code, which the end user
does not care about. I wish to overwrite the trace of the new
MyJobHandlerException instance with the trace from the originally caught
exception, but I cannot.

In this case the original exception should be set as the `$previous` exception for the `MyJobHandlerException` and then the resulting exception includes both the original's and the new exception's stack trace. Every exception handler worth its salt (including PHP's handler for uncaught exceptions) will print the entire exception stack: Online PHP editor | output for 2pc2Z

     <?php

     class MyJobHandlerException extends Exception {
         public function __construct(string $message, public readonly int $jobId, ?\Throwable $previous = null) {
             parent::__construct($message, previous: $previous);
         }
     }

     function execute_job(int $jobId) {
         throw new \Exception('execute_job failed');
     }

     function runner() {
         $jobId = 1;
         try {
             execute_job($jobId);
         } catch (\Throwable $e) {
             throw new MyJobHandlerException('Wrapping the exception', $jobId, $e);
         }
     }

     runner();

Best regards
Tim Düsterhus

On Mon, Jul 28, 2025 at 1:50 PM Rokas Šleinius <raveren@gmail.com> wrote:

On Mon, 28 Jul 2025 at 13:56, Tim Düsterhus <tim@bastelstu.be> wrote:

Hi

Am 2025-07-28 11:41, schrieb Dmitry Derepko:

A few years ago I found that a library printed error traces wrong.
After a little research I found that there was a mix of 3rd party
integration error + raised error around the current bridge
implementation
(http client).

There were several PHP applications with microservices architecture
which I
had access to (docker + sources).

So having the message and traces I’d like to have an error chain as it
can
be done chaining several errors through a new Exception(previous: $e).
But PHP does not allow you to manually implement Throwable. Instead you
should extend Exception. But after that you still cannot override the
getTrace() method because it’s final.

So my proposal is pretty simple: Remove both restrictions.

I’m afraid I don’t quite understand what actual goal you intend to solve
with the proposal. The description of your use case is very abstract,
can you provide a real-world example of a use-case you want to enable?

Best regards
Tim Düsterhus

| real-world example of a use-case you want to enable

Say I am implementing a job runner, I do its error handling, and I want to enrich the caught exception with additional helpful data for debugging. In this process I create a custom exception MyJobHandlerException, but the trace of the new exception includes my handler code, which the end user does not care about. I wish to overwrite the trace of the new MyJobHandlerException instance with the trace from the originally caught exception, but I cannot.

I was always sure that the reason behind all methods of \Exception being final is Larry-Garfield-blogpost length/depth so I never bothered to ask and made workarounds.

If I understand you correctly; You rethrow the exception, wrapping it in a custom exception class, and you’d like to have a final/composed stacktrace of all the exceptions?

https://3v4l.org/m7HIQ#vnull

On Mon, Jul 28, 2025, at 11:41, Dmitry Derepko wrote:

Hello PHP Internals,

I would like to propose a discussion regarding two current limitations in PHP’s exception handling system that I believe could be addressed to improve flexibility and developer experience.

A few years ago I found that a library printed error traces wrong.
After a little research I found that there was a mix of 3rd party integration error + raised error around the current bridge implementation (http client).

There were several PHP applications with microservices architecture which I had access to (docker + sources).

So having the message and traces I’d like to have an error chain as it can be done chaining several errors through a new Exception(previous: $e).
But PHP does not allow you to manually implement Throwable. Instead you should extend Exception. But after that you still cannot override the getTrace() method because it’s final.

So my proposal is pretty simple: Remove both restrictions.

  1. Allow user classes to implement Throwable interface directly

User classes cannot implement the Throwable interface now.

class MyCustomThrowable implements Throwable {}

// Fatal error: Class MyCustomThrowable cannot implement interface Throwable, extend Exception or Error instead
  1. Make getTrace() non-final or provide alternative customization mechanism

Exception traces cannot be overridden now.

class MyCustomThrowable extends Exception {
function getTrace() { return []; }
}

try { throw new MyCustomThrowable(); }
catch (Exception $e) { var_dump($e->getTrace()); }

// Fatal error: Cannot override final method Exception::getTrace()

There are some points about the feature:

  • Microservice support: Preserve traces across service boundaries
  • Proxy/decorator patterns: Maintain original error context through wrappers
  • Unified error handling: Any object implementing Throwable can be thrown consistently
  • Testing improvements: Create mock throwables for unit tests
  • Performance optimization: Avoid deep call stacks in generated traces

What do you think about it? Are there disadvantages or points to have the exceptions in the current state?

Best regards,

Dmitrii Derepko.
@xepozz

Wouldn’t a better approach be to allow serializing/deserializing exceptions?

— Rob

On Mon, 28 Jul 2025 at 15:16, Lynn <kjarli@gmail.com> wrote:

On Mon, Jul 28, 2025 at 1:50 PM Rokas Šleinius <raveren@gmail.com> wrote:

On Mon, 28 Jul 2025 at 13:56, Tim Düsterhus <tim@bastelstu.be> wrote:

Hi

Am 2025-07-28 11:41, schrieb Dmitry Derepko:

A few years ago I found that a library printed error traces wrong.
After a little research I found that there was a mix of 3rd party
integration error + raised error around the current bridge
implementation
(http client).

There were several PHP applications with microservices architecture
which I
had access to (docker + sources).

So having the message and traces I’d like to have an error chain as it
can
be done chaining several errors through a new Exception(previous: $e).
But PHP does not allow you to manually implement Throwable. Instead you
should extend Exception. But after that you still cannot override the
getTrace() method because it’s final.

So my proposal is pretty simple: Remove both restrictions.

I’m afraid I don’t quite understand what actual goal you intend to solve
with the proposal. The description of your use case is very abstract,
can you provide a real-world example of a use-case you want to enable?

Best regards
Tim Düsterhus

| real-world example of a use-case you want to enable

Say I am implementing a job runner, I do its error handling, and I want to enrich the caught exception with additional helpful data for debugging. In this process I create a custom exception MyJobHandlerException, but the trace of the new exception includes my handler code, which the end user does not care about. I wish to overwrite the trace of the new MyJobHandlerException instance with the trace from the originally caught exception, but I cannot.

I was always sure that the reason behind all methods of \Exception being final is Larry-Garfield-blogpost length/depth so I never bothered to ask and made workarounds.

If I understand you correctly; You rethrow the exception, wrapping it in a custom exception class, and you’d like to have a final/composed stacktrace of all the exceptions?

https://3v4l.org/m7HIQ#vnull

Yeah you guys come up with nice workarounds, I too, use a custom method on my exception class to get the “actually relevant” trace, but that’s not compatible with the world at large, and previous() or not, you cannot plug in your own trace (which was processed for user convenience).

Another example: what if I want to implement a userland job method failed(), where “the job system” would create a synthetic exception - but I want it to have the trace up to the actual line in “userland” code (where failed() was invoked) - and I don’t want my “system” calls in the trace.

Or another example: how Laravel handles ViewException - at one point the error handler builds a “fixed” trace where the compiled Blade files are replaced in the trace with the source blade.php files - as that is what is actually relevant to the user. I’m cloudy on the details of how exactly does it perform the switch to display the desired trace, but you can imagine it would be much nicer to have this code within the concern of the ViewException itself, but it can’t be done now.

On Mon, Jul 28, 2025, at 6:48 AM, Rokas Šleinius wrote:

| real-world example of a use-case you want to enable

Say I am implementing a job runner, I do its error handling, and I want
to enrich the caught exception with additional helpful data for
debugging. In this process I create a custom exception
MyJobHandlerException, but the trace of the new exception includes my
handler code, which the end user does not care about. I wish to
overwrite the trace of the new MyJobHandlerException instance with the
trace from the originally caught exception, but I cannot.

I was always sure that the reason behind all methods of \Exception
being final is Larry-Garfield-blogpost length/depth so I never bothered
to ask and made workarounds.

Hey, I resemble that remark! :stuck_out_tongue:

(I actually have no idea what the original reason was for making so much of Exceptions final.)

I'm not sure if this would cover what you're talking about, but I did start a discussion about lightweight exceptions a few months ago. I didn't go anywhere:

--Larry Garfield

Sure.

Imagine 2 PHP services:

  • backend monolith
  • mailer

At some condition the “backend” makes an HTTP request to the “mailer”.
The “mailer” answer with corresponding HTTP status, RPC body or in another way.

When something goes wrong mailer responses with:

  • status
  • error message
  • error trace

On the “backend” side we may inform the user that HTTP request failed:

  • check for response status
  • create an exception
    – with the error message in the “message” prop
    – and with the error trace in a custom prop, e.g. $externalTrace

Our exception renderer is quite powerful and renders all the chained errors one by one:

Error "$e->getMessage()" occurred on the line "$e->getLine()"...
- During handling another exception: "$e->getPrevious()->getMessage()" on the line "$e->getPrevious()->getLine()"...
-- During handling another exception: "$e->getPrevious()->getPrevious()->getMessage()"

and so on.

Unfortunately, dumping all the properties of an exception is not possible: there may be cycled references or just not readable bytes.
So, I should create a workaround (as many in this thread):
- Handle a particular class separately, reading a custom property, converting the traces to some PHP-like form
- Or create an interface for such exceptions
- Or append the traces to the message string
- Or just dump the traces to the log and make users to open logs

Instead, users may
- Construct an exception from the external structure: message and trace
- Raise an exception like "HttpClientException" and passing the exception from the previous point as a "previous" exception

After that an error renderer may render the traces as a regular chain of exceptions, even if there are traces that are not related to the current project.

Moreover, traces may come from another language like Go/Python/Java/etc. They still may be accessible and useful to the end user.

Of course I'm not talking about production systems and showing traces to everyone. As usual, they should be accessible only for developers.

This is my real case of using Temporal with a PHP server.
When something fails, you can access the traces from a custom exception property / get looong "message".

Here some code snippets:

function req($url, $params) {
$resp = $client->get($url, $params);
if ($resp->status > 300) {
$parsed = json_decode($resp->getBody());
$prev = new Exception(message: $parsed['message'], trace: $parsed['trace']); // or create a separate MyException and set trace there, or externally with setTrace()
throw new ClientHttpException("HTTP request to the $url was failed", previous: $prev); // chain traces to the 3rd party
}
}

{message: "...", trace: [...]} isn't contracted by PHP, a library should parse it itself.

<details class='elided'>
<summary title='Show trimmed content'>&#183;&#183;&#183;</summary>

Best regards,

Dmitrii Derepko.
@xepozz

</details>

It would look like another workaround to my case. Same as deserializing data into a class to write into a private property.
The simpler the better: just allow users to set their own trace. Or not set it at all.

···

Best regards,

Dmitrii Derepko.
@xepozz

Thank you for the cases. I’m not alone here with crazy traces :grinning_face:
Btw, I also have a “system” method which creates an exception and guess what?
The first trace line is placed in that file.

···

Best regards,

Dmitrii Derepko.
@xepozz

Wouldn’t a better approach be to allow serializing/deserializing exceptions?

— Rob

It would look like another workaround to my case. Same as deserializing data into a class to write into a private property.
The simpler the better: just allow users to set their own trace. Or not set it at all.

I still don’t understand what real life use case this solves. Maybe you already explained it but I didn’t get it. IMHO the trace should be set by the engine and it should not be possible to overwrite the getTrace method.

···

Best regards,

Dmitrii Derepko.
@xepozz

On 28/07/2025 16:27, Larry Garfield wrote:

(I actually have no idea what the original reason was for making so much of Exceptions final.)

If you start with everything marked "final", you can relax it as needed. For internal classes particularly, there can be implementation and performance penalties to letting users over-ride certain parts.

In this case, the trace is actually stored in a private property, and accessed separately by:

* getTrace()
* getTraceAsString()
* the default output for uncaught errors
* a couple of extensions that do weird hacks with it

Presumably, we would want all of these to polymorphically call getTrace() instead, to get the customised output. That might not be trivial, e.g. it means an error handler calling into userland code which could itself trigger errors.

If we don't provide that consistency, there's not much advantage over writing $trace = ($e instanceof ThrowableWithCustomisedTrace) ? $e->getCustomisedTrace() : $e->getTrace();

I wonder if there's actually an X/Y Problem here: is what is actually wanted more ways to affect what goes into the backtrace, or permanently edit it? We have #[SensitiveParameter], could we also have #[SkipCallInTrace]? Could we have a way to construct an exception with a custom trace?

As long as the format is correct when written to the private property, we don't need to change existing code that reads directly from that property.

--
Rowan Tommins
[IMSoP]

On Mon, Jul 28, 2025, at 19:54, Kamil Tekiela wrote:

On Mon 28 Jul 2025, 20:50 Dmitry Derepko, <xepozzd@gmail.com> wrote:

On Mon, Jul 28, 2025 at 3:26 PM Rob Landers rob@bottled.codes wrote:

Wouldn’t a better approach be to allow serializing/deserializing exceptions?

— Rob

It would look like another workaround to my case. Same as deserializing data into a class to write into a private property.
The simpler the better: just allow users to set their own trace. Or not set it at all.

Best regards,

Dmitrii Derepko.
@xepozz

I still don’t understand what real life use case this solves. Maybe you already explained it but I didn’t get it. IMHO the trace should be set by the engine and it should not be possible to overwrite the getTrace method.

I have a real life use case. I manage an SDK that does RPC. Exceptions from remote services are normalized no matter the language of the RPC. It would be nice to turn these into real exceptions like some of my colleagues do for the SDK other languages.

Right now, I simply throw a SyntheticException which encapsulates the original exception and displays the original stack trace as part of the message, but if I could manipulate the stack trace, that would be much more useful to users who end up displaying the php stack trace instead of the one from the remote system. Especially because a lot of frameworks like to truncate messages for some reason that is unknown to me.

At least, it would be nice to be able to create synthetic exceptions that don’t need to get a stack trace from the engine and could be 100% defined by the developer — or not, in Larry’s case.

— Rob

IMHO the trace should be set by the engine

Agreed. It should and it should continue to do it.

it should not be possible to overwrite the getTrace method.

Disagree. Not all traces can be created locally by the engine. 3rd-party traces are as useful as regular.

···

Best regards,

Dmitrii Derepko.
@xepozz

On Tue, 29 Jul 2025 at 01:53, Rob Landers rob@bottled.codes wrote:

On Mon, Jul 28, 2025, at 19:54, Kamil Tekiela wrote:

On Mon 28 Jul 2025, 20:50 Dmitry Derepko, <xepozzd@gmail.com> wrote:

On Mon, Jul 28, 2025 at 3:26 PM Rob Landers rob@bottled.codes wrote:

Wouldn’t a better approach be to allow serializing/deserializing exceptions?

— Rob

It would look like another workaround to my case. Same as deserializing data into a class to write into a private property.
The simpler the better: just allow users to set their own trace. Or not set it at all.

Best regards,

Dmitrii Derepko.
@xepozz

I still don’t understand what real life use case this solves. Maybe you already explained it but I didn’t get it. IMHO the trace should be set by the engine and it should not be possible to overwrite the getTrace method.

I have a real life use case. I manage an SDK that does RPC. Exceptions from remote services are normalized no matter the language of the RPC. It would be nice to turn these into real exceptions like some of my colleagues do for the SDK other languages.

Right now, I simply throw a SyntheticException which encapsulates the original exception and displays the original stack trace as part of the message, but if I could manipulate the stack trace, that would be much more useful to users who end up displaying the php stack trace instead of the one from the remote system. Especially because a lot of frameworks like to truncate messages for some reason that is unknown to me.

At least, it would be nice to be able to create synthetic exceptions that don’t need to get a stack trace from the engine and could be 100% defined by the developer — or not, in Larry’s case.

— Rob

› exceptions that don’t need to get a stack trace from the engine and could be 100% defined by the developer
FWIW, that totally solves all of my usecases I’ve ever encountered: new Exception('my message', trace: $myTrace)

Now since we’re talking, if \Exception had a setContext(array $context) + its getter, I’d be completely happy with it.

That is emulated in the “userland” of Laravel internals - and it also goes along with PSR-3 which defines a logger interface as

public function info($message, array $context = array());

And logging, to me at least, is in the same neighborhood as throwing exceptions - a lot of the time you want some attached data.

› Disagree. Not all traces can be created locally by the engine. 3rd-party traces are as useful as regular.
I think OP meant that there’s an internal implementation reason for getTrace() to not be allowed to be overriden.

Hello PHP Internals,

I would like to propose a discussion regarding two current limitations in PHP’s exception handling system that I believe could be addressed to improve flexibility and developer experience.

A few years ago I found that a library printed error traces wrong.
After a little research I found that there was a mix of 3rd party integration error + raised error around the current bridge implementation (http client).

There were several PHP applications with microservices architecture which I had access to (docker + sources).

So having the message and traces I’d like to have an error chain as it can be done chaining several errors through a new Exception(previous: $e).
But PHP does not allow you to manually implement Throwable. Instead you should extend Exception. But after that you still cannot override the getTrace() method because it’s final.

So my proposal is pretty simple: Remove both restrictions.

  1. Allow user classes to implement Throwable interface directly

User classes cannot implement the Throwable interface now.

class MyCustomThrowable implements Throwable {}

// Fatal error: Class MyCustomThrowable cannot implement interface Throwable, extend Exception or Error instead
  1. Make getTrace() non-final or provide alternative customization mechanism

The trace is stored in a private property, so you can use reflection to change it.

new \ReflectionProperty(\Exception::class, ‘trace’)->setValue($e, $trace)

Nicolas