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.
- 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
- 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
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:
https://3v4l.org/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
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?
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?
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.
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?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 sourceblade.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.
Thank you for the cases. I'm not alone here with crazy traces 😀
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
| 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! :-P
(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:
https://externals.io/message/127188
--Larry Garfield
(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]
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
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.
------
Moving further, there is an era of long-running apps. Coroutines also come
soon.
We may choose between classic exceptions with the "stack" trace and
modified exceptions with a custom-edited trace: prepend/append traces with
the coroutine-scope name or clean application bootstrap trace.
I'm not sure about mutating a complete trace of an exception, but still as
a case of usage.
--
Best regards,
Dmitrii Derepko.
@xepozz
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.
- 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
- 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
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
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.
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.
@xepozzI 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
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.
@xepozzI 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 https://www.php-fig.org/psr/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.
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.
Ok, what about a SyntheticException which will allow users to set
message/code/trace/line/file/etc from scratch?
Looks like a balanced solution between all our internal workarounds with
custom properties and classes and those who don't want to change the rest
of the mechanism of exceptions.
--
Best regards,
Dmitrii Derepko.
@xepozz
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.
- 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
- 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