Hi folks. In several recent RFCs and related discussion, the question of error handling has come up. Specifically, the problem is:
- "return null" conflicts with "but sometimes null is a real value" (the validity of that position is debatable, but common), and provides no useful information as to what went wrong.
- Exceptions are very expensive, the hierarchy is confusing, and handling them properly is a major pain. Failing to handle them properly is very easy since you have no way of knowing what exceptions the code you're calling might throw, or its nested calls, etc. That makes them poorly suited for mundane, predictable error conditions.
-
trigger_error()
is, well, a mess and not suitable for signaling to a calling function that something recoverable-in-context went wrong. - And... that's all we've got as options.
I've had an idea kicking around in my head for a while, which I know I've mentioned before. Given the timing, I want to put it out in its current unfinished form to see if there's interest in me bothering to finish it, or if it doesn't have a snowball's chance in hell of happening so it's not worth my time to further develop.
I know I've posted this before, but it's useful for background:
https://peakd.com/hive-168588/@crell/much-ado-about-null
https://joeduffyblog.com/2016/02/07/the-error-model/
From both prior discussions here as well as my understanding of language design trends, it seems the consensus view is that a Result type (aka, an Either monad) is the ideal mechanism for robust error handling. However, it requires generics to be really viable, which we don't have. It's also very clumsy to use in a classic-OOP language (like PHP) without special dedicated syntax.
Various languages work around that in various ways. Rust built its whole error system on Result types, and later added the ?
operator to indicate "and if this returns an error result, just return it directly", making delegating error handling vastly easier. Kotlin (via its Arrow library) relies on heavy use of chained tail-closures. Go has a convention of a "naked either" using two return values, but doesn't have any special syntax for it leading to famously annoying boilerplate. Python has lightweight exceptions so that throwing them willy nilly as a control flow tool is actually OK and Pythonic.
However, as noted in the "Error Model" article above, and this is key, a Result type is isomorphic to a checked exception. A checked exception is one where a function must explicitly declare what it can throw, and if it throws something else it's the function's error, and a compile time error. It also means any "bubbling" of exceptions has to be explicit at each function step. That's in contrast to unchecked exceptions, as PHP has now, which may be thrown from nearly anywhere and will silently bubble up and crash the program if not otherwise handled.
The key point here is that a happy-path return and an unhappy-but-not-world-ending-path need to be different. Using the return value for both (what returning null does) is simply insufficient.
The "Error Model" article goes into the pros and cons of checked vs unchecked exceptions so I won't belabor the point, except to say that most arguments against checked exceptions are based on Java's very-broken implementation of checked-except-when-it's-not exceptions. But as noted, what Rust and Go do is checked exceptions, aka a Result type, just spelled differently. The advantage of checked exceptions is that we don't need generics at all, and still get all the benefits. We can also design syntax around them specifically to make them more ergonomic.
I am invisioning something like this:
function div(int $n, int $d): float raises ZeroDivisor
{
if ($d === 0) {
raise new ZeroDivisor(); // This terminates the function.
}
return $n/$d;
}
The "raises" declaration specifies a class or interface type that could be "raised". It can be any object; no required Exception hierarchy, no backtrace, just a boring old object value. Enum if you feel like it, or any other object. We could probably allow union or full DNF types there if we wanted, though I worry that it may lead to too confusing of an API. (To bikeshed later.) Static analysis tools could very easily detect if the code doesn't match up with the declared raises.
This feature already exists in both Midori (the subject of the "Error Model" article) and Swift. So it's not a new invention; in fact it's quite old.
The handling side is where I am still undecided on syntax. Swift uses essentially try-catch blocks, though I fear that would be too verbose in practice and would be confused with existing "heavy" exceptions. Midori did the same.
Various ideas I've pondered in no particular order:
// Suck it up and reuse try-catch
function test() { // No declared raise, so if it doesn't handle ZeroDivisor itself, fatal.
try {
$val = div(3, 0);
} catch (ZeroDivisor $e) {
print "Nope.";
}
}
// try-catch spelled differently to avoid confusion with exceptions
try {
$val = div(3, 0);
} handle (ZeroDivisor $e) {
print "Nope.";
}
// Some kind of suffix block, maybe with a specially named variable?
$val = div(3, 0) else { print $err->message; return 0; }
// A "collapsed" try-catch block.
$val = try div(3, 0)
catch (ZeroDivisor $e) { print "Nope"; }
catch (SomethingElse $e) { print "Wat?"; }
// Similar to Rust's ? operator, to make propagating an error easier.
// The raise here could be the same or wider than what div() raises.
function test(): float raises ZeroDivisor {
$val = div(3, 0) reraise;
// use $val safely knowing it was returned and nothing was raised.
}
Or other possibilities I've not considered.
The use cases for a dedicated error channel are many:
- Any variation of "out of bounds": Could be "record not found in database", or "no such array key" or "you tried to get the first item of an empty list", or many other things along those lines.
- Expected input validation errors. This would cover the URL/URI RFC's complex error messages, without the C-style "inout" parameter.
- Chaining validation. A series of validators that can return true (or just the value being validated) OR raise an object with the failure reason. A wrapping function can collect them all into a single error object to return to indicate all the various validation failures.
- A transformer chain, which does the same as validation but passes on the transformed value and raises on the first erroring transformer.
Exceptions remain as is, for "stop the world" unexpected failures or developer errors (bugs). But mundane errors, where local resolution is both possible and appropriate, get a dedicated channel and syntax with no performance overhead. That also naturally becomes a Python-style "better to beg forgiveness than ask permission" approach to error handling if desired, without all the overhead of exceptions.
So that's what I've got so far. My question for the audience is:
-
Assuming we could flesh out a comfortable and ergonomic syntax, would you support this feature, or would you reject it out of hand?
-
For engine-devs: Is this even feasible? :-) And if so, anyone want to join me in developing it?
-
And least relevant, I'm very open to suggestions for the syntax, though the main focus right now is question 1 to determine if discussing syntax is even worthwhile.
--
Larry Garfield
larry@garfieldtech.com
Hi folks. In several recent RFCs and related discussion, the question of error handling has come up. Specifically, the problem is:
- "return null" conflicts with "but sometimes null is a real value" (the validity of that position is debatable, but common), and provides no useful information as to what went wrong.
- Exceptions are very expensive, the hierarchy is confusing, and handling them properly is a major pain. Failing to handle them properly is very easy since you have no way of knowing what exceptions the code you're calling might throw, or its nested calls, etc. That makes them poorly suited for mundane, predictable error conditions.
trigger_error()
is, well, a mess and not suitable for signaling to a calling function that something recoverable-in-context went wrong.- And... that's all we've got as options.
I've had an idea kicking around in my head for a while, which I know I've mentioned before. Given the timing, I want to put it out in its current unfinished form to see if there's interest in me bothering to finish it, or if it doesn't have a snowball's chance in hell of happening so it's not worth my time to further develop.
I know I've posted this before, but it's useful for background:
https://peakd.com/hive-168588/@crell/much-ado-about-null
https://joeduffyblog.com/2016/02/07/the-error-model/From both prior discussions here as well as my understanding of language design trends, it seems the consensus view is that a Result type (aka, an Either monad) is the ideal mechanism for robust error handling. However, it requires generics to be really viable, which we don't have. It's also very clumsy to use in a classic-OOP language (like PHP) without special dedicated syntax.
Various languages work around that in various ways. Rust built its whole error system on Result types, and later added the
?
operator to indicate "and if this returns an error result, just return it directly", making delegating error handling vastly easier. Kotlin (via its Arrow library) relies on heavy use of chained tail-closures. Go has a convention of a "naked either" using two return values, but doesn't have any special syntax for it leading to famously annoying boilerplate. Python has lightweight exceptions so that throwing them willy nilly as a control flow tool is actually OK and Pythonic.However, as noted in the "Error Model" article above, and this is key, a Result type is isomorphic to a checked exception. A checked exception is one where a function must explicitly declare what it can throw, and if it throws something else it's the function's error, and a compile time error. It also means any "bubbling" of exceptions has to be explicit at each function step. That's in contrast to unchecked exceptions, as PHP has now, which may be thrown from nearly anywhere and will silently bubble up and crash the program if not otherwise handled.
The key point here is that a happy-path return and an unhappy-but-not-world-ending-path need to be different. Using the return value for both (what returning null does) is simply insufficient.
The "Error Model" article goes into the pros and cons of checked vs unchecked exceptions so I won't belabor the point, except to say that most arguments against checked exceptions are based on Java's very-broken implementation of checked-except-when-it's-not exceptions. But as noted, what Rust and Go do is checked exceptions, aka a Result type, just spelled differently. The advantage of checked exceptions is that we don't need generics at all, and still get all the benefits. We can also design syntax around them specifically to make them more ergonomic.
I am invisioning something like this:
function div(int $n, int $d): float raises ZeroDivisor { if ($d === 0) { raise new ZeroDivisor(); // This terminates the function. } return $n/$d; }
The "raises" declaration specifies a class or interface type that could be "raised". It can be any object; no required Exception hierarchy, no backtrace, just a boring old object value. Enum if you feel like it, or any other object. We could probably allow union or full DNF types there if we wanted, though I worry that it may lead to too confusing of an API. (To bikeshed later.) Static analysis tools could very easily detect if the code doesn't match up with the declared raises.
This feature already exists in both Midori (the subject of the "Error Model" article) and Swift. So it's not a new invention; in fact it's quite old.
The handling side is where I am still undecided on syntax. Swift uses essentially try-catch blocks, though I fear that would be too verbose in practice and would be confused with existing "heavy" exceptions. Midori did the same.
Various ideas I've pondered in no particular order:
// Suck it up and reuse try-catch function test() { // No declared raise, so if it doesn't handle ZeroDivisor itself, fatal. try { $val = div(3, 0); } catch (ZeroDivisor $e) { print "Nope."; } }
// try-catch spelled differently to avoid confusion with exceptions try { $val = div(3, 0); } handle (ZeroDivisor $e) { print "Nope."; }
// Some kind of suffix block, maybe with a specially named variable? $val = div(3, 0) else { print $err->message; return 0; }
// A "collapsed" try-catch block. $val = try div(3, 0) catch (ZeroDivisor $e) { print "Nope"; } catch (SomethingElse $e) { print "Wat?"; }
// Similar to Rust's ? operator, to make propagating an error easier. // The raise here could be the same or wider than what div() raises. function test(): float raises ZeroDivisor { $val = div(3, 0) reraise; // use $val safely knowing it was returned and nothing was raised. }
Or other possibilities I've not considered.
The use cases for a dedicated error channel are many:
- Any variation of "out of bounds": Could be "record not found in database", or "no such array key" or "you tried to get the first item of an empty list", or many other things along those lines.
- Expected input validation errors. This would cover the URL/URI RFC's complex error messages, without the C-style "inout" parameter.
- Chaining validation. A series of validators that can return true (or just the value being validated) OR raise an object with the failure reason. A wrapping function can collect them all into a single error object to return to indicate all the various validation failures.
- A transformer chain, which does the same as validation but passes on the transformed value and raises on the first erroring transformer.
Exceptions remain as is, for "stop the world" unexpected failures or developer errors (bugs). But mundane errors, where local resolution is both possible and appropriate, get a dedicated channel and syntax with no performance overhead. That also naturally becomes a Python-style "better to beg forgiveness than ask permission" approach to error handling if desired, without all the overhead of exceptions.
So that's what I've got so far. My question for the audience is:
Assuming we could flesh out a comfortable and ergonomic syntax, would you support this feature, or would you reject it out of hand?
For engine-devs: Is this even feasible? :-) And if so, anyone want to join me in developing it?
And least relevant, I'm very open to suggestions for the syntax, though the main focus right now is question 1 to determine if discussing syntax is even worthwhile.
--
Larry Garfield
larry@garfieldtech.com
Hey Larry,
I’m still digesting this, but I wonder if this problem (errors vs non-world-ending errors vs happy path) is a problem due to people often making warnings into exceptions?
I feel like many devs/frameworks “abuse” (for lack of a better word) the error handling system to make everything into a world-ending-error when it is not.
Even over the last few versions of PHP, more and more warnings have turned into exceptions that maybe ought not be.
Overall, this has resulted in a somewhat more inconsistent language than it was previously. For example, you used to be able to consider an array as a field of infinite nulls. You could iterate over an infinite set of keys and get nothing but null. This made it easier to explain to junior devs at the time. Now, you have to explain it as a hash-map-but-sometimes-array-thing that emits warnings when a key doesn’t exist but will still give you null.
We are currently in this weird place between “infinite field of nulls” and “sparse array/hash-map” but not quite either one — depending on what you do with that warning.
It would be great if we could pick one and I think it would also solve the whole “is null an error or a value” problem.
I personally wouldn’t like checked exceptions. I have PTSD from Java’s, so just hearing that phrase gives me flashbacks of catching dozens of potential exceptions. In Go, yes there is some boilerplate, but it’s not that bad and it easy to deal with, especially with IDE support.
As for a Result type, that’s already possible in user-land; what does bringing it to the engine get us? Is it just a lightweight exception without a trace to prevent the cost of generation? Why not just have a pool of exceptions that you can just modify the message?
— Rob
Hey Larry,
I’m still digesting this, but I wonder if this problem (errors vs
non-world-ending errors vs happy path) is a problem due to people often
making warnings into exceptions?I feel like many devs/frameworks “abuse” (for lack of a better word)
the error handling system to make everything into a world-ending-error
when it is not.
Yes, it's common for devs today to use exceptions in inappropriate ways. I've done it myself at times. The problem is that we don't have a tool fit-for-purpose for "your input is invalid in a totally predictable way." So you can either squeeze an error code into the return value (null or otherwise), which sucks, or abuse exceptions, which sucks. That's exactly the issue I want to solve, by offering a tool more purpose built for that case.
Even over the last few versions of PHP, more and more warnings have
turned into exceptions that maybe ought not be.Overall, this has resulted in a somewhat more inconsistent language
than it was previously. For example, you used to be able to consider an
array as a field of infinite nulls. You could iterate over an infinite
set of keys and get nothing but null. This made it easier to explain to
junior devs at the time. Now, you have to explain it as a
hash-map-but-sometimes-array-thing that emits warnings when a key
doesn’t exist but will still give you null.We are currently in this weird place between “infinite field of nulls”
and “sparse array/hash-map” but not quite either one — depending on
what you do with that warning.
Treating arrays as an infinite series of nulls, however... no, that's always been a developer error. If that was the mental model you were working from, it's an incorrect mental model, and it always has been, since forever. Arguably it's PHP's fault for letting you develop that broken mental model because it didn't complain loudly; both PHP and MySQL started life with lots of "we'll quietly assume what you probably meant and not tell you that you're wrong" features, and both have had a very long, very painful process of correcting that fundamental mistake. Anyone working from an "infinite series of nulls" model has been an unfortunate victim of that design flaw.
It's possible we could use light-checked exceptions as a better way of signaling "hey, you're reading an invalid key". So far I've only considered them as a function-boundary thing, but we could probably look into raising them implicitly for certain read operations. (Maybe only on objects that are using Gina's forthcoming Fetch interfaces? I dunno.)
It would be great if we could pick one and I think it would also solve
the whole “is null an error or a value” problem.
Just having an "alternate null" is not viable for many reasons, which I go into in the article I linked before so I won't repeat it here.
I personally wouldn’t like checked exceptions. I have PTSD from Java’s,
so just hearing that phrase gives me flashbacks of catching dozens of
potential exceptions. In Go, yes there is some boilerplate, but it’s
not that bad and it easy to deal with, especially with IDE support.
As the Midori article I linked explains, the PTSD you have from Java exceptions is not an issue with checked exceptions; it's an issue with Java's terribly designed exception system, combined with all values being inherently nullable.
A well-designed checked exception system doesn't "force you to handle all these error cases". A well-designed system "tells you every error case that you need to think about, and helps you deal with it." Smart usage of interfaces on the error values can make dealing with a whole class of issues easier, and some more ergonomic syntax than try-catch (such as Rust's ?) can make it less cumbersome.
In a sense, checked exceptions show you exactly what you need to do in order to turn a partial function (that just dies on certain values in its input range) into a total function with two return channels (because you'll always get back a legit value, just sometimes the legit value is on the error pathway.) That both surfaces the unhappy paths (that any robust code should handle) and simplifies handling them.
As for a Result type, that’s already possible in user-land; what does
bringing it to the engine get us? Is it just a lightweight exception
without a trace to prevent the cost of generation? Why not just have a
pool of exceptions that you can just modify the message?
A general purpose Result type requires generics, or else you lose all type info on what the real return value's type is. We don't have generics, so that's simply not an option. The separate channel approach gives us all the benefits of a Result type without the need for generics, and it's standardized in the language.
A general purpose Result type is also very clumsy to use in practice, unless you have syntax optimized for working with it. Rust has its match statement (which supports pattern matching, decomposition, and multi-line statements specifically for dissecting a Result type) and ? operator, which is a one-character way to "pass on" the error to the caller, as though it were an unchecked exception. For PHP, we'd probably want a different set of syntax tools as our working model is different than Rust's. What those would be, I don't know yet. Hence this thread.
--Larry Garfield
Hi,
Reading this as a PHP dev is confusing (terminology-wise) because errors
used to be the fatal ("stop the world") conditions while exceptions were the
catchable, recovarable issues within some call - feels to me pretty
equivalent to what you're proposing here.
What's the problem with PHP exceptions? I'm not even trying to argue, I'm
trying to understand. Is it the implementation (bad/expensive performance)?
Semantics? Handling syntax?
I didn't understand the bubbling changes either. What do you mean by
"fatal"? Does it turn the raised "light error" into a real exception? Or
does it turn into an actual error crashing the whole process?
function div(int $n, int $d): float raises ZeroDivisor
{
if (!$d)
raise new ZeroDivisor;
return $n/$d;
}
function inverse(int $x): float
{
// does not handle the raise
return div(1, $x);
}
// assuming the above functions are in a lib, can i rescue this using
some trycatch or not?
inverse($_GET['x']);
And if I don't rescue, am I supposed to see that I passed a bad value to
inverse
, a bad value to div
or just that inverse
failed to handle a
raise?
BR,
Juris
What's the problem with PHP exceptions? I'm not even trying to argue, I'm
trying to understand. Is it the implementation (bad/expensive performance)?
Semantics? Handling syntax?
Larry provided several problems in the opening of his email; you may
want to reread that.
I will affirm that exceptions can be quite expensive. Throwing one
means constructing a backtrace, walking the entire call stack to
gather at minimum function names, file names, and line numbers, and
optionally information about arguments. This is why using them for
errors that are expected to be handled is often considered
inappropriate.
Hi,
Reading this as a PHP dev is confusing (terminology-wise) because errors
used to be the fatal ("stop the world") conditions while exceptions were the
catchable, recovarable issues within some call - feels to me pretty
equivalent to what you're proposing here.What's the problem with PHP exceptions? I'm not even trying to argue, I'm
trying to understand. Is it the implementation (bad/expensive performance)?
Semantics? Handling syntax?
There's two key problems with exceptions as PHP currently has them:
- Creating and attaching the backtrace to them is stupidly expensive. It's one of the most expensive things you can ask the engine to do, I think.
- They're unchecked, which means as a developer calling a function you have absolutely no idea what exceptions it might throw without reading every line of that function and every function it calls, transitively. That means it's extremely easy for an exception to kill the process if no one expects to catch it.
What I am proposing is something that works somewhat like exceptions, but without either of those problems, to handle cases where the immediate caller is the obvious responsible party to handle the error case. "Heavy" exceptions will continue to be useful for "the database caught fire" type errors, where crashing the whole system is reasonable and it's just a question of how to do so gracefully.
I didn't understand the bubbling changes either. What do you mean by
"fatal"? Does it turn the raised "light error" into a real exception? Or
does it turn into an actual error crashing the whole process?
The details here are one of the things I haven't thought through yet (hence the point of the email, which is asking "is it worth my time to think through these things?") Most likely, an unhandled raise or an invalid raise would get converted to a thrown Error of some kind, which would be handleable like any other Error (which is generally "log and then crash"). That situation would be a "the developer screwed up" bug 100% of the time, so a thrown Error is the correct response.
--Larry Garfield
There's two key problems with exceptions as PHP currently has them:
- Creating and attaching the backtrace to them is stupidly expensive.
It's one of the most expensive things you can ask the engine to do, I
think.
Would an EmptyBacktraceException solve it then? Ideally a LightException
would gather the call stack in it's backtrace as it bubbles up until
it's caught. It migth solve most of the legit reporting needs with an
insignificant performance impact. No idea if it's doable though.
- They're unchecked, which means as a developer calling a function you
have absolutely no idea what exceptions it might throw without reading
every line of that function and every function it calls, transitively.
That means it's extremely easy for an exception to kill the process if
no
one expects to catch it.
Why wouldn't these raises end up with the same issue? I call
inverse($x)
of a lib, it calls div(1, $x)
internally, fails to
handle the raise and I end up with a crash/exception of the old kind.
Or is inverse
forced to check every possible raise of div
? But
that's probably clumsy, sometimes redundant and not something that can
be enforced during compilation. So we still end up with runtime
exceptions from somewhere deep in the lib, but instead of "ZeroDivisor"
they are "unhandled raise" exceptions?
Sorry, I guess this somewhat overlaps with the bubbling portion that you
haven't thought through, but I'm continuing to ask because it seems I'm
not the only one having hard time to grasp the feeling of the proposed
model.
BR,
Juris
Good afternoon, Larry.
Looking at the comparison table, it seems that the two most important
differences are:
Backtrace consumes a lot of resources.
2.
There is an explicit contract for exceptions thrown by a function.
3.
I didn't fully understand the point about the exception hierarchy, but
it seems important too.
It seems to me that the Backtrace issue is a problem of a low level of
abstraction — the implementation of exceptions. That's one problem. The
lack of an explicit contract is a problem on a completely different level
of abstraction.
The issue with the missing contract could have been solved even for
exceptions, without creating a new entity.
Regarding Backtrace, the following questions seem fair:
What if I want to know where the situation occurred? Can I just ignore
this information?
2.
If yes, why not create an option to disable backtrace generation for
exceptions?
Regarding the Result/Error type.
I have experience using this approach in remote services, where exceptions
seem inappropriate.
It’s probably possible to use it even now without generics, and without any
special language features.
What if we focus on:
Improving exception handling, making it as lightweight as in Python.
2.
Introducing explicit exception contracts.
Best Regards, Ed.
Good afternoon, Larry.
Looking at the comparison table, it seems that the two most important differences are:
Backtrace consumes a lot of resources.
There is an explicit contract for exceptions thrown by a function.
I didn't fully understand the point about the exception hierarchy, but it seems important too.
It seems to me that the Backtrace issue is a problem of a low level of abstraction — the implementation of exceptions. That's one problem. The lack of an explicit contract is a problem on a completely different level of abstraction.
The issue with the missing contract could have been solved even for exceptions, without creating a new entity.
Regarding Backtrace, the following questions seem fair:
What if I want to know where the situation occurred? Can I just ignore this information?
If yes, why not create an option to disable backtrace generation for exceptions?
Regarding the Result/Error type.
I have experience using this approach in remote services, where exceptions seem inappropriate.
It’s probably possible to use it even now without generics, and without any special language features.What if we focus on:
Improving exception handling, making it as lightweight as in Python.
Introducing explicit exception contracts.
Best Regards, Ed.
I'm not going to lie, I spent nearly an hour last night attempting to create an exception without a stack trace. I was quite surprised at how impossible it is. The engine really goes out of its way to ensure the trace is set.
A simple solution (for this problem) may be creating a new exception type \LightException or something that would allow for never setting a stack trace. This is pretty heavily on the "pragmatic" side of the solution space, but it is also relatively simple.
— Rob
Good afternoon, Larry.
Looking at the comparison table, it seems that the two most important differences are:
Backtrace consumes a lot of resources.
There is an explicit contract for exceptions thrown by a function.
I didn't fully understand the point about the exception hierarchy, but it seems important too.
It seems to me that the Backtrace issue is a problem of a low level of abstraction — the implementation of exceptions. That's one problem. The lack of an explicit contract is a problem on a completely different level of abstraction.
The issue with the missing contract could have been solved even for exceptions, without creating a new entity.
Regarding Backtrace, the following questions seem fair:
What if I want to know where the situation occurred? Can I just ignore this information?
For the type of failure we are talking about, there is not really the
need for a backtrace.
If yes, why not create an option to disable backtrace generation for exceptions?
Regarding the Result/Error type.
I have experience using this approach in remote services, where exceptions seem inappropriate.
It’s probably possible to use it even now without generics, and without any special language features.What if we focus on:
Improving exception handling, making it as lightweight as in Python.
Introducing explicit exception contracts.
Best Regards, Ed.
I'm not going to lie, I spent nearly an hour last night attempting to create an exception without a stack trace. I was quite surprised at how impossible it is. The engine really goes out of its way to ensure the trace is set.
One thing you can do is throw the same exception again.
Not really a good solution though.
A simple solution (for this problem) may be creating a new exception type \LightException or something that would allow for never setting a stack trace. This is pretty heavily on the "pragmatic" side of the solution space, but it is also relatively simple.
The main obstacle here is interface Throwable which promises to have a
file, line number and backtrace.
Currently you can only throw objects that implement Throwable.
https://3v4l.org/LHhNm
If we introduce a "LightException", it cannot really extend Exception,
because then it gets all the baggage.
The solution could be a new interface "Catchable" or "Raisable". But
this would be superfluous.
Instead, we could allow to throw any objects, or introduce a new
keyword "raise", that would allow any objects.
On the implementing side, a "raise new NotFound(..)" seems mostly the
same as "throw new NotFound(..)".
On the declaration side, we might want to add " ... throws NotFound"
or "... raises NotFound", to make this part of the contract. So this
would be very similar to checked exceptions as I know them from Java.
On the calling (and handling) side, any alternative we find to "try
... catch (...) ..." might also be useful for regular exceptions.
In fact this would be the big difference to current exceptions and
return values in terms of developer experience, whereas the "no stack
trace overhead" is more an internal thing.
--- Andreas
— Rob
Hello, Rob.
I was quite surprised at how impossible it is.
static zend_object *zend_default_exception_new(zend_class_entry
*class_type) /* {{{ */
{
....
if (EG(current_execute_data)) {
zend_fetch_debug_backtrace(
It's not that this is 100% impossible. PHP is a language with an execution
context, which means PHP can hypothetically change behavior depending on
the context. This means it's entirely possible to imagine code like:
if (EG(without_trace)) {}
One could follow Python's approach and create the backtrace only when the
exception is thrown. But this isn't a particularly elegant solution.
Besides, in 90% of cases, an exception is created to be thrown.
Use a deferred Backtrace generation algorithm? Such an algorithm is almost
impossible, because the backtrace is built based on the execution context,
which will be changed once the function returns control.
There are only two realistic options:
Either don't use a backtrace at all, or use a minimal version (only FILE
- LINE).
Generate the backtrace only when the programmer explicitly requests it,
but then accept that some information will be missing.
For example:
raise $exception — does not generate a backtrace.
throw $exception — generates a backtrace, but only from the point where
the exception is thrown.
A lightweight backtrace might be imperfect in accurately indicating the
call path if multiple functions are on the same line, but otherwise it
saves a lot of memory and CPU.
The raise/throw option might also make sense. But it has more nuances and
contradictions.
The HHVM project suggests an interesting Lazy backtrace generation
mechanism based on stack unwinding.
An exception is generated.
2.
The stack enters an unwinding mode. In this mode, PHP gradually collects
information into the backtrace.
3.
Each frame exit contributes a part of the backtrace.
4.
Once the exception is caught, the unwinding process stops.
5.
Now a decision can be made: either generate the full backtrace or
suppress the exception.
Advantages:
Allows deferring backtrace generation until the exception is caught.
2.
Allows suppressing exceptions without a backtrace, making their creation
cost cheap.
3.
Allows preserving exceptions with a backtrace only when necessary.
Disadvantages: requires added complexity.
I'm going to respond to points raised by several people together; I'm using Ed's message as a starting point but this is also i response to Niels, Rob, and Andreas.
Good afternoon, Larry.
Looking at the comparison table, it seems that the two most important
differences are:
- Backtrace consumes a lot of resources.
Yes. And even if it can be made faster (as it looks like Niels is doing, which is great), it will never be as fast as an empty constructor and a return. That's the level I'm proposing.
- There is an explicit contract for exceptions thrown by a function.
Yes.
- I didn't fully understand the point about the exception hierarchy,
but it seems important too.
I somewhat glossed over this point, but let me expand on it here.
Exceptions currently have a Java-inspired hierarchy. Everything MUST extend either Error or Exception. The available exceptions are spread across core and SPL, but internals is not supposed to use the SPL ones.
For the use cases I'm talking about, "InvalidArgumentException" is the most common case, I expect. Or rather, special cases of that. However, it extends LogicException, which is specified in the docs as "Exception that represents error in the program logic." Which is... wrong, because half the time or more an InvalidArgumentException is a case of validating user data that cannot be fully validated by the type system... and thus not really a programmer error. It's only a programmer error if they don't handle it gracefully.
Moreover, all exceptions currently track:
- message
- code
- file
- line
- trace
- previous exception
Those are useful when they show up in logs read by a human. For the use cases I am describing, none of them are relevant, useful, or desireable. These values are for programmatic use only. But all of those are baked into the exception system at its core. Just having a third "kind" of exception (in addition to extends Error
and extends Exception
) would not really solve anything.
The issue with the missing contract could have been solved even for
exceptions, without creating a new entity.
This is incorrect, as making the current exception system checked would be a ginormous BC break. And having some throwables be checked and some not, but using the same syntax and class hierarchy... well, that's the reason everyone detests Java's exceptions. Let's not do that.
Regarding Backtrace, the following questions seem fair:
- What if I want to know where the situation occurred? Can I just
ignore this information?
That is completely and utterly irrelevant, just as it is completely and utterly irrelevant on which line a return
statement appeared.
I think the framing of this concept as "lighter exceptions" is the wrong lens. I probably contributed to that in my initial explanation, so let me try and clarify:
What I am proposing is not "Better exceptions." It's "a second kind of return value." It's closer to Rust's Result types or Go's multi-returns, but spelled differently. That is, it turns out (see the Error Model article previously), logically identical to checked exceptions. But if the mental model of "what is an exception" is what PHP has today, then that is more misleading than helpful. So let's not talk of that further.
Rather, consider this example:
class Repo {
public function getUser(int $id): User {
$record = $this->db->query("...")->fetchOne();
if ($record) {
return new User(...$record);
}
// Uh, now what?
}
}
There's various ways to handle that case. Sticking an exception at the end of the method is one option, but as I've pointed out, a particularly bad one. "That user isn't here" is not an error case that should be able to silently crash the application, just because someone put an invalid number in a URL.
We could make the return value ?User and then return null, but now we have to manually check for null every frickin' time we call the method, or we get random null errors in who knows where. And it doesn't let us differentiate between "user not found" and "that is a negative int, which is never correct you idiot."
Suppose a hypothetical world where we had generics:
// We literally need nothing on these classes other than their type.
// More complex cases might, but in this case, anything more than this is a waste.
interface RepoErr {}
class NoSuchUser implements RepoErr {}
class NegativeId implements RepoErr {}
class Repo {
public function getUser(int $id): Result<User, RepoErr> {
if ($id <= 0) {
return new Result::err(new NegativeId());
}
$record = $this->db->query("...")->fetchOne();
if ($record) {
return new Result::ok(new User(...$record));
}
return new Result::err(new NoSuchUser());
}
}
And then to use it, you MUST do:
function doStuff($id) {
$ret = $repo->getUser($id);
if ($user instanceof OK) {
$user = $ret->value;
} else {
if ($ret->err instanceof NoSuchUser) {
display_user_message('Who is that?');
return;
} else if ($ret->err instanceof NegativeId) {
display_user_message('Buddy, that's not a thing.");
return;
}
}
// If you got here, it means the getUser call way way way up there was valid.
}
I think it's reasonably obvious that is extremely clumsy, which is why almost no one in PHP does it, including FP stans like me. Plus that whole generics thing.
What I've started doing is using union returns as a sort of "naked result":
class Repo {
public function getUser(int $id): User|UserErr {
if ($id <= 0) {
return new new NegativeId();
}
$record = $this->db->query("...")->fetchOne();
if ($record) {
return new User(...$record);
}
return new NoSuchUser();
}
}
function doStuff($id) {
$user = $repo->getUser($id);
if ($user instanceof UserErr) {
...
}
$user is now usable.
}
But that relies on me always remembering to do that check, and means static analysis tools can't know what the type of $user is until I check it. Better, still not great.
What I am proposing is to take the Result type example and change the spelling to:
class Repo {
// We get reliable type information without generics!
public function getUser(int $id): User raises UserErr {
if ($id <= 0) {
raise new NegativeId();
}
$record = $this->db->query("...")->fetchOne();
if ($record) {
return new User(...$record);
}
raise new NoSuchUser();
}
}
And then something reasonable on the receiving side, which I've not fully thought through yet. The whole point of this thread is "should I take the time to think that through?" :-)
If we used try-catch or something structured the same, then we at least get:
function doStuff($id) {
try {
$user = $repo->getUser($id);
// Code that uses $user, which is guaranteed to be a User.
return ...;
} catch (NoSuchUser) { // Note not capturing the value, as we don't need it in this case.
display_user_message('Who is that?');
} catch (NegativeId) {
display_user_message('Buddy, that's not a thing.");
}
}
I'm not convinced that's the right syntax, but it's a possible syntax. If you want to defer handling of a particular error to the caller, then you would explicitly do this:
function doStuff($id): string raises UserErr {
try {
$user = $repo->getUser($id);
// Code that uses $user, which is guaranteed to be a User.
return ...;
} catch (UserErr $e) {
raise $e;
}
Which is why I think we do want some kind of syntax similar to Rust's ?, so the above could be shortened back to this:
function doStuff($id): string raises UserErr {
$user = $repo->getUser($id) reraise;
// We have a good user.
}
If you try to raise an error from a function that doesn't specify it... that is exactly the same as trying to return an array from that function. return array
would be a type error on the success channel. raise new ProductErr
would be a type error on the failure channel. Same idea.
Again, I don't think try-catch in its current form is ideal. I'm not sure what is. I'm trying to decide if it would be a waste of my time to figure out what would be better. But again, this is not an exception. This is a second return channel, aka a different way to spell a Result type, such that we don't need a clumsy Result type.
--Larry Garfield
I'm going to respond to points raised by several people together; I'm using Ed's message as a starting point but this is also i response to Niels, Rob, and Andreas.
Good afternoon, Larry.
Looking at the comparison table, it seems that the two most important
differences are:
- Backtrace consumes a lot of resources.
Yes. And even if it can be made faster (as it looks like Niels is doing, which is great), it will never be as fast as an empty constructor and a return. That's the level I'm proposing.
- There is an explicit contract for exceptions thrown by a function.
Yes.
- I didn't fully understand the point about the exception hierarchy,
but it seems important too.I somewhat glossed over this point, but let me expand on it here.
Exceptions currently have a Java-inspired hierarchy. Everything MUST extend either Error or Exception. The available exceptions are spread across core and SPL, but internals is not supposed to use the SPL ones.
For the use cases I'm talking about, "InvalidArgumentException" is the most common case, I expect. Or rather, special cases of that. However, it extends LogicException, which is specified in the docs as "Exception that represents error in the program logic." Which is... wrong, because half the time or more an InvalidArgumentException is a case of validating user data that cannot be fully validated by the type system... and thus not really a programmer error. It's only a programmer error if they don't handle it gracefully.
Moreover, all exceptions currently track:
- message
- code
- file
- line
- trace
- previous exception
Those are useful when they show up in logs read by a human. For the use cases I am describing, none of them are relevant, useful, or desireable. These values are for programmatic use only. But all of those are baked into the exception system at its core. Just having a third "kind" of exception (in addition to
extends Error
andextends Exception
) would not really solve anything.The issue with the missing contract could have been solved even for
exceptions, without creating a new entity.This is incorrect, as making the current exception system checked would be a ginormous BC break. And having some throwables be checked and some not, but using the same syntax and class hierarchy... well, that's the reason everyone detests Java's exceptions. Let's not do that.
Regarding Backtrace, the following questions seem fair:
- What if I want to know where the situation occurred? Can I just
ignore this information?That is completely and utterly irrelevant, just as it is completely and utterly irrelevant on which line a
return
statement appeared.I think the framing of this concept as "lighter exceptions" is the wrong lens. I probably contributed to that in my initial explanation, so let me try and clarify:
What I am proposing is not "Better exceptions." It's "a second kind of return value." It's closer to Rust's Result types or Go's multi-returns, but spelled differently. That is, it turns out (see the Error Model article previously), logically identical to checked exceptions. But if the mental model of "what is an exception" is what PHP has today, then that is more misleading than helpful. So let's not talk of that further.
Rather, consider this example:
class Repo {
public function getUser(int $id): User {
$record = $this->db->query("...")->fetchOne();
if ($record) {
return new User(...$record);
}
// Uh, now what?
}
}There's various ways to handle that case. Sticking an exception at the end of the method is one option, but as I've pointed out, a particularly bad one. "That user isn't here" is not an error case that should be able to silently crash the application, just because someone put an invalid number in a URL.
We could make the return value ?User and then return null, but now we have to manually check for null every frickin' time we call the method, or we get random null errors in who knows where. And it doesn't let us differentiate between "user not found" and "that is a negative int, which is never correct you idiot."
Suppose a hypothetical world where we had generics:
// We literally need nothing on these classes other than their type.
// More complex cases might, but in this case, anything more than this is a waste.
interface RepoErr {}
class NoSuchUser implements RepoErr {}
class NegativeId implements RepoErr {}class Repo {
public function getUser(int $id): Result<User, RepoErr> {
if ($id <= 0) {
return new Result::err(new NegativeId());
}
$record = $this->db->query("...")->fetchOne();
if ($record) {
return new Result::ok(new User(...$record));
}
return new Result::err(new NoSuchUser());
}
}And then to use it, you MUST do:
function doStuff($id) {
$ret = $repo->getUser($id);
if ($user instanceof OK) {
$user = $ret->value;
} else {
if ($ret->err instanceof NoSuchUser) {
display_user_message('Who is that?');
return;
} else if ($ret->err instanceof NegativeId) {
display_user_message('Buddy, that's not a thing.");
return;
}
}
// If you got here, it means the getUser call way way way up there was valid.
}I think it's reasonably obvious that is extremely clumsy, which is why almost no one in PHP does it, including FP stans like me. Plus that whole generics thing.
What I've started doing is using union returns as a sort of "naked result":
class Repo {
public function getUser(int $id): User|UserErr {
if ($id <= 0) {
return new new NegativeId();
}
$record = $this->db->query("...")->fetchOne();
if ($record) {
return new User(...$record);
}
return new NoSuchUser();
}
}function doStuff($id) {
$user = $repo->getUser($id);
if ($user instanceof UserErr) {
...
}
$user is now usable.
}But that relies on me always remembering to do that check, and means static analysis tools can't know what the type of $user is until I check it. Better, still not great.
What I am proposing is to take the Result type example and change the spelling to:
class Repo {
// We get reliable type information without generics!
public function getUser(int $id): User raises UserErr {
if ($id <= 0) {
raise new NegativeId();
}
$record = $this->db->query("...")->fetchOne();
if ($record) {
return new User(...$record);
}
raise new NoSuchUser();
}
}And then something reasonable on the receiving side, which I've not fully thought through yet. The whole point of this thread is "should I take the time to think that through?" :-)
If we used try-catch or something structured the same, then we at least get:
function doStuff($id) {
try {
$user = $repo->getUser($id);
// Code that uses $user, which is guaranteed to be a User.
return ...;
} catch (NoSuchUser) { // Note not capturing the value, as we don't need it in this case.
display_user_message('Who is that?');
} catch (NegativeId) {
display_user_message('Buddy, that's not a thing.");
}
}I'm not convinced that's the right syntax, but it's a possible syntax. If you want to defer handling of a particular error to the caller, then you would explicitly do this:
function doStuff($id): string raises UserErr {
try {
$user = $repo->getUser($id);
// Code that uses $user, which is guaranteed to be a User.
return ...;
} catch (UserErr $e) {
raise $e;
}Which is why I think we do want some kind of syntax similar to Rust's ?, so the above could be shortened back to this:
function doStuff($id): string raises UserErr {
$user = $repo->getUser($id) reraise;
// We have a good user.
}If you try to raise an error from a function that doesn't specify it... that is exactly the same as trying to return an array from that function.
return array
would be a type error on the success channel.raise new ProductErr
would be a type error on the failure channel. Same idea.Again, I don't think try-catch in its current form is ideal. I'm not sure what is. I'm trying to decide if it would be a waste of my time to figure out what would be better. But again, this is not an exception. This is a second return channel, aka a different way to spell a Result type, such that we don't need a clumsy Result type.
--Larry Garfield
Hmmm,
Reminds me of working on wordpress's backend, where you would write something like
function get_user(): WP_User|WP_Error
-- or something like that (it's been a long time).
But if it was an exceptional error, you'd just throw. But, you'd have to write something like this every time you called it:
if (($user = get_user()) instanceof WP_Error) { /* handle error */ }
// $user is WP_User
What you're suggesting is basically providing this via a separate "track" or "channel" so it would look like:
function get_user(): WP_User raises WP_Error {}
$user = get_user() reraise;
I understand that these are mostly lightweight, programatical errors, not exceptions. So, for example, running out of disk space, "not found" results, etc. These are things you should just handle ... not report. However, there are cases where these do become exceptional further up the stack. For example, if you are supposed to be storing a profile image and you cannot recover from a full disk or a user that the client thinks exists -- you probably want to turn them into an exception. Maybe something like this:
$user = get_user() reraise (WP_Error as LogicException);
where you can specify an error to be wrapped in an exception. The stack trace and everything would come from this line, not where the error actually came from. That shouldn't be an issue though, in most code.
— Rob
Hello all.
Yes. And even if it can be made faster (as it looks like Niels is doing,
which is great), it will never be as fast as an empty constructor and a
return. That's the level I'm proposing.
If the backtrace is generated only when needed, rather than at the moment
the exception is created, there will be no difference or almost no
difference (if a stack unwinding algorithm is used).
The total number of CPU/memory operations will be approximately the same.
The only difference is that deferred backtrace generation will require more
complex frame return code, which may introduce pitfalls.
However, if a backtrace is needed by itself, such overhead is unavoidable
regardless of whether exceptions are used or not.
I somewhat glossed over this point, but let me expand on it here.
So the exception hierarchy in PHP is far from ideal. Understood. The
hierarchy can be improved to make it more logical and consistent. And as I
understand it, changing the exceptions themselves is not required for
that.
Moreover, all exceptions currently track:
Yes. And the main problem among them is the trace property. Neither file
nor line cause significant overhead, because file is a reference to a
string constant, and line is an integer.
This is incorrect, as making the current exception system checked would
be a ginormous BC break.
And having some throwables be checked and some not, but using the same
syntax and class hierarchy... well, that's the reason everyone detests
Java's exceptions. Let's not do that.
While introducing a new entity, such as a "New Error Stream" with defined
contract rules, is theoretically possible,
it would not fundamentally change the current situation.
In practice, this approach would introduce a second category of exceptions,
requiring developers to manage:
the primary execution flow,
the standard exception flow,
a "special exception flow" that must be converted back into the standard
one when necessary.
This added complexity may outweigh the potential benefits, especially given
that the primary value would be limited to preserving backward
compatibility.
It is worth carefully considering whether such a change justifies the
additional mental and technical overhead.
On the other side of the scale:
replacing @throws with a special syntax (or perhaps simply introducing a
new attribute?),
defining the behavior in case of contract violation while preserving
backward compatibility.
For example, if an exception contract is violated, an error would not be
thrown; instead, a warning would be issued, which would not break the
program's execution flow but would help draw the developer’s attention to
the issue.
Later in the letter you explain in more detail that this is not a special
kind of exception, nor a new execution flow, but rather a* special type of
result*.
But if this is a special type of result, then it should follow the same
rules as all PHP types. In other words, this cannot be solved without
generics.
However, the benefit of the new syntax, which could make the code cleaner,
does not depend on generics:
For example:
$res = someFunction() catch ($err) {throw $err;} // Like Zig?
But then the return type must be Throwable.
The advantage of generics like Result<> is that they do not create a
separate return channel.
Everything operates within a single flow.
However, this approach is not yet possible in PHP.
Introducing an additional return channel would mean increasing the overall
complexity.
Of course, this is a clean and useful syntax for such cases.
Moreover, the catch block could be made mandatory if the function is marked
with a contract — meaning the function cannot be called without a catch
block at all.
However, it seems to me that we can achieve the same result using throw,
simply by adding new syntax and capabilities. Yes, there may be some
backward compatibility issues, but is it really something to be afraid of?
Edmond Dantes edmond.ht@gmail.com hat am 28.04.2025 13:46 CEST geschrieben:
Hello all.
Yes. And even if it can be made faster (as it looks like Niels is doing, which is great), it will never be as fast as an empty constructor and a return. That's the level I'm proposing.
If the backtrace is generated only when needed, rather than at the moment the exception is created, there will be no difference or almost no difference (if a stack unwinding algorithm is used).
The total number of CPU/memory operations will be approximately the same. The only difference is that deferred backtrace generation will require more complex frame return code, which may introduce pitfalls.
However, if a backtrace is needed by itself, such overhead is unavoidable regardless of whether exceptions are used or not.I somewhat glossed over this point, but let me expand on it here.
So the exception hierarchy in PHP is far from ideal. Understood. The hierarchy can be improved to make it more logical and consistent. And as I understand it, changing the exceptions themselves is not required for that.
Moreover, all exceptions currently track:
Yes. And the main problem among them is the trace property. Neither file nor line cause significant overhead, because file is a reference to a string constant, and line is an integer.
This is incorrect, as making the current exception system checked would be a ginormous BC break.
And having some throwables be checked and some not, but using the same syntax and class hierarchy... well, that's the reason everyone detests Java's exceptions. Let's not do that.While introducing a new entity, such as a "New Error Stream" with defined contract rules, is theoretically possible,
it would not fundamentally change the current situation.In practice, this approach would introduce a second category of exceptions, requiring developers to manage:
the primary execution flow,
the standard exception flow,
a "special exception flow" that must be converted back into the standard one when necessary.
This added complexity may outweigh the potential benefits, especially given that the primary value would be limited to preserving backward compatibility.
It is worth carefully considering whether such a change justifies the additional mental and technical overhead.
On the other side of the scale:
replacing @throws with a special syntax (or perhaps simply introducing a new attribute?),
defining the behavior in case of contract violation while preserving backward compatibility.
For example, if an exception contract is violated, an error would not be thrown; instead, a warning would be issued, which would not break the program's execution flow but would help draw the developer’s attention to the issue.
Later in the letter you explain in more detail that this is not a special kind of exception, nor a new execution flow, but rather a special type of result.
But if this is a special type of result, then it should follow the same rules as all PHP types. In other words, this cannot be solved without generics.
However, the benefit of the new syntax, which could make the code cleaner, does not depend on generics:
For example:$res = someFunction() catch ($err) {throw $err;} // Like Zig?
But then the return type must be Throwable.
The advantage of generics like Result<> is that they do not create a separate return channel.
Everything operates within a single flow.
However, this approach is not yet possible in PHP.Introducing an additional return channel would mean increasing the overall complexity.
Of course, this is a clean and useful syntax for such cases.
Moreover, the catch block could be made mandatory if the function is marked with a contract — meaning the function cannot be called without a catch block at all.However, it seems to me that we can achieve the same result using throw, simply by adding new syntax and capabilities. Yes, there may be some backward compatibility issues, but is it really something to be afraid of?
Hello,
looking into userland code, I often see usages for file, line, class, function, etc. without args.
So there are a few options:
-
Set zend.exception_ignore_args by default to 1.
-
Add a new attribute for custom classes:
#[\ExceptionSkipTraceArgs]
class BreakException extends \Exception implements Exception
- Implement a new LightException (or similar name) with no args in getTrace().
Regards
Thomas
- Implement a new LightException (or similar name) with no args in
getTrace().
- or 4) Deferred backtrace mechanism:
- Does not compute the backtrace when the exception is created.
- The backtrace is fully computed only if the exception is caught using a
catch block or by a global handler when reaching the bottom of the stack. - The backtrace may not be computed at all if the exception is caught for
suppression (possibly requiring separate syntax).
There is a technical problem here.
When a user saves an exception object somewhere and later tries to retrieve
the trace.
If a solution to this can be found, then no changes would be needed on the
UserLand side at all. ...
- Implement a new LightException (or similar name) with no args in
getTrace().
- or 4) Deferred backtrace mechanism:
- Does not compute the backtrace when the exception is created.
- The backtrace is fully computed only if the exception is caught using a
catch block or by a global handler when reaching the bottom of the stack.- The backtrace may not be computed at all if the exception is caught for
suppression (possibly requiring separate syntax).There is a technical problem here.
When a user saves an exception object somewhere and later tries to
retrieve the trace.
If a solution to this can be found, then no changes would be needed on the
UserLand side at all. ...
Clarification.
It is possible to link the lifetime of an exception to the lifetime of the
frame in which the exception was caught or thrown.
If the frame is destroyed but the exception is not, then the backtrace must
be generated.
If the exception is destroyed first, backtrace generation is not needed.
The overall algorithm is as follows:
An exception is created. The trace is not generated. The exception is
linked to the frame where it was thrown.
2.
If the exception is thrown, the stack unwinding mode is activated.
3.
If the exception is not thrown but the frame is being destroyed, a full
backtrace is generated before the frame is destroyed.
4.
In stack unwinding mode, the backtrace is built step by step until the
exception is handled in a catch block.
5.
If the frame is destroyed while the exception still exists, a full
backtrace is generated; otherwise, nothing happens.
Later in the letter you explain in more detail that this is not a
special kind of exception, nor a new execution flow, but rather a*
special type of result*.But if this is a special type of result, then it should follow the
same rules as all PHP types. In other words, this cannot be solved
without generics.
I do not understand that statement in the slightest. I have already demonstrated how it can be solved without generics. Multiple response channels from a function already exist: Normal returns and exceptions. Exceptions as currently designed are just very poorly suited to the problem space I am describing.
However, the benefit of the new syntax, which could make the code
cleaner, does not depend on generics:
For example:$res = someFunction() catch ($err) {throw $err;} // Like Zig?
A way to simplify try-catch syntax is certainly a possible side effect of this feature, though that is secondary to the point I am making.
However, it seems to me that we can achieve the same result using
throw
, simply by adding new syntax and capabilities. Yes, there may
be some backward compatibility issues, but is it really something to be
afraid of?
See other threads going on right now debating what BC breaks are acceptable and which are not. And let's not forget that every release there is an outcry of "OMG you broke my app!" for even the smallest deprecation of something widely acknowledged to be bad practice anyway. We should absolutely not be flippant about BC breaks in behavior.
--Larry Garfield
I have already demonstrated how it can be solved without generics.
Multiple response channels from a function already exist: Normal returns
and exceptions. Exceptions as currently designed are just very poorly
suited to the problem space I am describing.
If another error channel is introduced, PHP would end up with two error
channels.
This is harder to understand than the Normal returns + Exception channels +
Something new.
It seems to me that this creates a bigger problem than the one it is trying
to solve.
Is it possible to avoid creating two error channels and instead design an
alternative handling method that would eliminate the drawbacks of the first
approach?
If the performance issue is solved, are the other two problems really worth
such changes?
For example, if we follow the philosophy of RESULT<X>, we can say that
there are two types of exception handling cases:
Suppressing an exception immediately at the first level of function call.
2.
Stack unwinding.
For example, the expression:
function someFunction(): string raises SomeException {
throw SomeException()
}
// The backtrace is not generated:
$res = try someFunction() catch (SomeException) null;
// The backtrace is generated when throw $err executed.
$res = try someFunction() catch ($err) {throw $err};
// The backtrace is generated
$res = someFunction();
This kind of behavior doesn’t break BC, right? At the same time, it allows
improving PHP performance and adding contracts.
I have already demonstrated how it can be solved without generics. Multiple response channels from a function already exist: Normal returns and exceptions. Exceptions as currently designed are just very poorly suited to the problem space I am describing.
If another error channel is introduced, PHP would end up with two
error channels.
This is harder to understand than the Normal returns + Exception
channels + Something new.It seems to me that this creates a bigger problem than the one it is
trying to solve.Is it possible to avoid creating two error channels and instead design
an alternative handling method that would eliminate the drawbacks of
the first approach?If the performance issue is solved, are the other two problems really
worth such changes?For example, if we follow the philosophy of
RESULT<X>
, we can say
that there are two types of exception handling cases:
- Suppressing an exception immediately at the first level of function
call.
Suppressing? You mean fataling. Suppressing implies ignore, which is the exact opposite of what I am proposing.
- Stack unwinding.
Given the choice between:
success return + error-return + the-world-is-broken
vs
success return + the-world-is-broken + the-world-is-broken-but-not-really-so-you-have-to-handle-it
The first seems substantially better, frankly.
Exceptions that are sometimes checked and sometimes not is Java, which is exactly why everyone hates Java's exception system.
function someFunction(): string raises SomeException { throw SomeException() } // The backtrace is not generated: $res = try someFunction() catch (SomeException) null; // The backtrace is generated when throw $err executed. $res = try someFunction() catch ($err) {throw $err}; // The backtrace is generated $res = someFunction();
This kind of behavior doesn’t break BC, right? At the same time, it
allows improving PHP performance and adding contracts.
The particulars there in that syntax imply the only catch handling would be defining a different value, which may not be the desired handling.
Also, that again runs into "throw may be checked or not, you don't always know."
It also means the exception is carrying around the extra design baggage of exceptions. Even if the trace is elided, it's still carrying useless metadata that would lead people astray. It also means dealing with the constructor of Exceptions, which is notoriously annoying for extending.
"Make exceptions nicer" is fundamentally not what I am proposing, nor will I propose as a solution for this space. It is the wrong approach. (Making exceptions nicer is well and good in its own right, and I won't block that, but it does not address this problem space.)
It's clear you do not support what I am proposing. Good to know, thanks. I still haven't gotten any feedback from major voters, though, so leaving this thread open.
--Larry Garfield
On Mon, Apr 28, 2025, 1:22 p.m. Larry Garfield larry@garfieldtech.com
wrote:
I have already demonstrated how it can be solved without generics.
Multiple response channels from a function already exist: Normal returns
and exceptions. Exceptions as currently designed are just very poorly
suited to the problem space I am describing.If another error channel is introduced, PHP would end up with two
error channels.
This is harder to understand than the Normal returns + Exception
channels + Something new.It seems to me that this creates a bigger problem than the one it is
trying to solve.Is it possible to avoid creating two error channels and instead design
an alternative handling method that would eliminate the drawbacks of
the first approach?If the performance issue is solved, are the other two problems really
worth such changes?For example, if we follow the philosophy of
RESULT<X>
, we can say
that there are two types of exception handling cases:
- Suppressing an exception immediately at the first level of function
call.Suppressing? You mean fataling. Suppressing implies ignore, which is the
exact opposite of what I am proposing.
- Stack unwinding.
Given the choice between:
success return + error-return + the-world-is-broken
vs
success return + the-world-is-broken +
the-world-is-broken-but-not-really-so-you-have-to-handle-itThe first seems substantially better, frankly.
Exceptions that are sometimes checked and sometimes not is Java, which is
exactly why everyone hates Java's exception system.function someFunction(): string raises SomeException { throw SomeException() } // The backtrace is not generated: $res = try someFunction() catch (SomeException) null; // The backtrace is generated when throw $err executed. $res = try someFunction() catch ($err) {throw $err}; // The backtrace is generated $res = someFunction();
This kind of behavior doesn’t break BC, right? At the same time, it
allows improving PHP performance and adding contracts.The particulars there in that syntax imply the only catch handling would
be defining a different value, which may not be the desired handling.Also, that again runs into "throw may be checked or not, you don't always
know."It also means the exception is carrying around the extra design baggage of
exceptions. Even if the trace is elided, it's still carrying useless
metadata that would lead people astray. It also means dealing with the
constructor of Exceptions, which is notoriously annoying for extending."Make exceptions nicer" is fundamentally not what I am proposing, nor will
I propose as a solution for this space. It is the wrong approach. (Making
exceptions nicer is well and good in its own right, and I won't block that,
but it does not address this problem space.)It's clear you do not support what I am proposing. Good to know, thanks.
I still haven't gotten any feedback from major voters, though, so leaving
this thread open.--Larry Garfield
I fail to see the value in this, seems to be solving a problem I've never
encountered. Who cares what baggage exceptions carry? They are meant for
exceptional situations, seems like you're trying to transform exceptions
into something entirely different under the guise of improving them.
- Hammed
On Mon, Apr 28, 2025, 1:22 p.m. Larry Garfield larry@garfieldtech.com
wrote:I have already demonstrated how it can be solved without generics.
Multiple response channels from a function already exist: Normal returns
and exceptions. Exceptions as currently designed are just very poorly
suited to the problem space I am describing.If another error channel is introduced, PHP would end up with two
error channels.
This is harder to understand than the Normal returns + Exception
channels + Something new.It seems to me that this creates a bigger problem than the one it is
trying to solve.Is it possible to avoid creating two error channels and instead design
an alternative handling method that would eliminate the drawbacks of
the first approach?If the performance issue is solved, are the other two problems really
worth such changes?For example, if we follow the philosophy of
RESULT<X>
, we can say
that there are two types of exception handling cases:
- Suppressing an exception immediately at the first level of function
call.Suppressing? You mean fataling. Suppressing implies ignore, which is
the exact opposite of what I am proposing.
- Stack unwinding.
Given the choice between:
success return + error-return + the-world-is-broken
vs
success return + the-world-is-broken +
the-world-is-broken-but-not-really-so-you-have-to-handle-itThe first seems substantially better, frankly.
Exceptions that are sometimes checked and sometimes not is Java, which is
exactly why everyone hates Java's exception system.function someFunction(): string raises SomeException { throw SomeException() } // The backtrace is not generated: $res = try someFunction() catch (SomeException) null; // The backtrace is generated when throw $err executed. $res = try someFunction() catch ($err) {throw $err}; // The backtrace is generated $res = someFunction();
This kind of behavior doesn’t break BC, right? At the same time, it
allows improving PHP performance and adding contracts.The particulars there in that syntax imply the only catch handling would
be defining a different value, which may not be the desired handling.Also, that again runs into "throw may be checked or not, you don't always
know."It also means the exception is carrying around the extra design baggage
of exceptions. Even if the trace is elided, it's still carrying useless
metadata that would lead people astray. It also means dealing with the
constructor of Exceptions, which is notoriously annoying for extending."Make exceptions nicer" is fundamentally not what I am proposing, nor
will I propose as a solution for this space. It is the wrong approach.
(Making exceptions nicer is well and good in its own right, and I won't
block that, but it does not address this problem space.)It's clear you do not support what I am proposing. Good to know,
thanks. I still haven't gotten any feedback from major voters, though, so
leaving this thread open.--Larry Garfield
I fail to see the value in this, seems to be solving a problem I've never
encountered. Who cares what baggage exceptions carry? They are meant for
exceptional situations, seems like you're trying to transform exceptions
into something entirely different under the guise of improving them.Two rules I try to consider when it comes to exceptions:
- Exceptions are expensive computationally, because they trigger
a backtrace on instantiation. - Exceptions should not be used for normal application logic flow. If the
"error" is recoverable and/or expected, use a different mechanism so you
can use standard conditional branching.
As such, there are a lot of situations where I may not want to use
exceptions. Two common ones:
- Input validation. In most cases, invalid input is expected, and a
condition you will handle in your code. Exceptions are a really poor
mechanism for this. - "Not found" conditions, such as not finding a matching row in a database
or a cache. Again, this is expected, and something you should handle via
conditionals.
Currently, there's no generic way to handle these sorts of error
conditions from functions. You can create "result" types specific to each
operation, but this is a lot of boilerplate. You can resort to exceptions,
but these are a poor fit due to performance and logic flow.
I'm currently not yet convinced on the proposal Larry is making, but I
definitely understand what's driving it, and would love a language-level
solution.
--
Matthew Weier O'Phinney
mweierophinney@gmail.com
https://mwop.net/
he/him
- Exceptions should not be used for normal application logic flow. If
the "error" is recoverable and/or expected, use a different mechanism
so you can use standard conditional branching.As such, there are a lot of situations where I may not want to use
exceptions. Two common ones:
- Input validation. In most cases, invalid input is expected, and a
condition you will handle in your code. Exceptions are a really poor
mechanism for this.- "Not found" conditions, such as not finding a matching row in a
database or a cache. Again, this is expected, and something you should
handle via conditionals.
I don't want to make this into a quarrel, please consider this to be a
genuine question -- I'm trying to understand the viewpoint behind the
need for such "failed result" channel.
I'm considering this scenario: An update request comes into a controller
and passes a superficial validation of field types. The 'troller invokes
an action which in turn invokes a service or whatever the chain is.
Somewhere along the call stack once all the data is loaded we realize
that the request was invalid all along, e.g. the status can't be changed
to X because that's not applicable for objects of kind B that have
previously been in status Z.
In such situations I have found (according to my experience) the
following solution to be a good, robust and maintainable pattern:
Once I find the request was invalid, I throw a ValidationException. No
matter how deep in the stack I am. No matter that the callers don't know
I might've thrown that. The exception will be caught and handled by some
boundary layer (controller, middleware, error handler or whatever),
formatted properly and returned to the user in a request-appropriate
form.
I currently have no urge to return an indication of invalidity manually
and pass it up the call stack layer by layer. Should I want that? In my
experience such patterns (requiring each layer to do an if
for the
possible issue and return up the stack instead of continuing the
execution) get very clumsy for complex actions. Or have I misunderstood
the usecase that you had in mind?
BR,
Juris
- Exceptions should not be used for normal application logic flow. If the "error" is recoverable and/or expected, use a different mechanism so you can use standard conditional branching.
As such, there are a lot of situations where I may not want to use exceptions. Two common ones:
- Input validation. In most cases, invalid input is expected, and a condition you will handle in your code. Exceptions are a really poor mechanism for this.
- "Not found" conditions, such as not finding a matching row in a database or a cache. Again, this is expected, and something you should handle via conditionals.
I don't want to make this into a quarrel, please consider this to be a
genuine question — I'm trying to understand the viewpoint behind the
need for such "failed result" channel.I'm considering this scenario: An update request comes into a
controller and passes a superficial validation of field types. The
'troller invokes an action which in turn invokes a service or whatever
the chain is. Somewhere along the call stack once all the data is
loaded we realize that the request was invalid all along, e.g. the
status can't be changed to X because that's not applicable for objects
of kind B that have previously been in status Z.In such situations I have found (according to my experience) the
following solution to be a good, robust and maintainable pattern:Once I find the request was invalid, I throw a ValidationException. No
matter how deep in the stack I am. No matter that the callers don't
know I might've thrown that. The exception will be caught and handled
by some boundary layer (controller, middleware, error handler or
whatever), formatted properly and returned to the user in a
request-appropriate form.I currently have no urge to return an indication of invalidity manually
and pass it up the call stack layer by layer. Should I want that? In my
experience such patterns (requiring each layer to do anif
for the
possible issue and return up the stack instead of continuing the
execution) get very clumsy for complex actions. Or have I misunderstood
the usecase that you had in mind?BR,
Juris
The key distinction is here:
Somewhere along the call stack once all the data is
loaded we realize that the request was invalid all along
combined with:
No matter that the callers don't
know I might've thrown that.
Addressing the second part first, unchecked exceptions means I have no idea at all if an exception is going to get thrown 30 calls down the stack from me. Literally any line in my function that calls anything could be the last. Is my code ready for that? Can it handle that? Or do I need to put a try-finally inside every function just in case?
Admittedly in a garbage collected language that concern is vastly reduced, to the point most people don't think about that concern. But that's not because it's gone away entirely. Are you writing a file and need to write a terminal line to it before closing the handle? Are you in the middle of a DB transaction that isn't using a closure wrapper for auto-closing (which PDO natively does not)?
Technically, if you're writing this code:
$pdo->beginTransaction();
foreach ($something as $val) {
$write = transform($val);
$pdo->query('write $write here');
}
$pdo->commit();
That's unsafe because transform() might throw, and if it does, the commit() line is never reached. So you really have to put it in a try-catch-finally. (Usually people push that off to a wrapping closure these days, but that introduces extra friction and you have to know to do it.)
Or similarly,
$fp = fopen('out.csv', 'w');
fwrite($fp, "Header here");
foreach ($input as $data) {
$line = transform($data);
fputcsv($fp, $line);
}
fwrite($fp, "Footer here");
fclose($fp);
If transform() throws on the 4th entry, you now have an incomplete file written to disk. And literally any function you could conceive of could do this to you. The contract of every function is implicitly "and I might also unwind the call stack 30 levels at any time and crash the program, cool?"
You are correct that unchecked exceptions let you throw from way down in the stack if something goes wrong later. Which brings us back to the first point: If that's a problem, it's a code smell that you should be validating your data sooner. This does naturally lead to a different architectural approach. Error return channels are an "in the small" feature. They're intended to make the contract between one function and another more robust. One can build a system-wide error pattern out of them, but they're fundamentally an in-the-small feature.
So in the example you list, I would offer:
- Do more than "superficial validation" at the higher level. Validate field types and that a user is authorized to write this value (for example).
- As discussed, we do need some trivially easy way to explicitly defer an error value back to our caller. That should be easy enough that it's not a burden to do, but still explicit enough that both the person reading and writing the code know that an error is being propagated. This still requires research to determine what would work best for PHP.
Wouldn't the heavy use of error return channels create additional friction in some places and cause us to shift how we write code? Yes, grasshopper, that is the point. :-) The language should help us write robust, error-proof code, and the affordances and frictions should naturally nudge us in that direction. Just as explicit typing makes certain patterns less comfortable and therefore we move away from them, and are better for it.
--Larry Garfield
Hello.
Suppressing? You mean fataling. Suppressing implies ignore, which is the exact opposite of what I am proposing.
Yes, it seems I didn’t explain my thought clearly.
We have the following code:
function some(): bool
{
return false;
}
function target(): void
{
if(some() === false) {
// error occurred, some logic if error
}
}
The point of this code is that we handle the error immediately at the
first level.
If we rewrite this using exceptions, it means we’ll be handling the
exception at the first level from where it was thrown.
success return + error-return + the-world-is-broken
By "the-world-is-broken," does that mean "throw"?
And why is throw better than return? Why?
Exceptions that are sometimes checked and sometimes not is Java, which is exactly why everyone hates Java's exception system.
That phrase can’t be considered an argument, because it’s unclear who
“everyone” is (I’m not part of that group), why they hate something,
and why we should care about their negative emotions.
Maybe they’re wrong?
Exceptions are a technique with both advantages and disadvantages.
For business-oriented languages, the advantages outweigh the
drawbacks, because there's less code.
Less code means fewer bugs.
Less code also means lower coupling, which makes refactoring cheaper.
Cheaper refactoring makes business development easier.
In programming, it’s often the case that something becomes trendy, and
old tools start getting hate just because some new-but-actually-old
approaches have appeared.
But in practice, trends fade, the fog clears, and proven methods keep
working. :)
It's clear you do not support what I am proposing.
Why can’t I partially agree and partially disagree? The world isn’t
black and white.
I agree with the questions you raised — each of them is valid on its own.
But I think it’s worth reflecting on the implementation. And Occam’s
razor should be applied here.
Best Regards, Ed.
Which is why I think we do want some kind of syntax similar to Rust's ?, so the above could be shortened back to this:
function doStuff($id): string raises UserErr {
$user = $repo->getUser($id) reraise;
// We have a good user.
}
One thing about Rust's ?, compared with an additional "reraise" keyword
thingy, is that the former is inline with the rest of the expression
while the latter forces a distinct statement for each possible failure
point. The "happy path" no longer looks quite so happy.
In other words, Rust's approach looks syntactically a lot more like
PHP's "?->" nullsafe access, which can be looked on as addressing the
specific case of "returning null to indicate failure" approach to error
handling (in the even more specific case where the happy path would have
returned an object).
Which is why I think we do want some kind of syntax similar to Rust's ?, so the above could be shortened back to this:
function doStuff($id): string raises UserErr {
$user = $repo->getUser($id) reraise;
// We have a good user.
}One thing about Rust's ?, compared with an additional "reraise" keyword
thingy, is that the former is inline with the rest of the expression
while the latter forces a distinct statement for each possible failure
point. The "happy path" no longer looks quite so happy.In other words, Rust's approach looks syntactically a lot more like
PHP's "?->" nullsafe access, which can be looked on as addressing the
specific case of "returning null to indicate failure" approach to error
handling (in the even more specific case where the happy path would have
returned an object).
One of the related ideas I had (but omitted from the initial post for brevity) was to require error objects to implement a new marker interface, which would cause the object to behave like null as far as nullsafe operators were concerned. Or possibly some other set of operators, I'm not sure. But some way to allow error objects to behave differently in a convenient way. (This is the sort of thing I've not worked out yet because I don't know if it's worth it.)
--Larry Garfield
- Exceptions are very expensive, the hierarchy is confusing, and handling them properly is a major pain. Failing to handle them properly is very easy since you have no way of knowing what exceptions the code you're calling might throw, or its nested calls, etc. That makes them poorly suited for mundane, predictable error conditions.
I get the point of having checked exceptions, and I suppose it would be nice.
Hierarchy: I don't think the hierarchy is that confusing, can you elaborate?
Regarding performance however, rather than introducing yet another completely new concept to do almost the same thing, why not try to improve exception performance instead?
I just opened a PR that makes instantiating exceptions much much faster, and this is only after like 15 mins of work. I'm sure there's even more to gain.
Kind regards
Niels
- Exceptions are very expensive, the hierarchy is confusing, and
handling them properly is a major pain. Failing to handle them properly is
very easy since you have no way of knowing what exceptions the code you're
calling might throw, or its nested calls, etc. That makes them poorly
suited for mundane, predictable error conditions.
I get the point of having checked exceptions, and I suppose it would be
nice.
Hierarchy: I don't think the hierarchy is that confusing, can you
elaborate?
Regarding performance however, rather than introducing yet another
completely new concept to do almost the same thing, why not try to improve
exception performance instead?
Came here to say the same.
Also: Either
/Option
/Maybe
types, with some basic generics, even if
only at documentation + PHPStan/Psalm level?
I'm coming from months of Go development, and I really just want my
exceptions (with traces by default) back, tbh, even if it means performance
suffers.
I just opened a PR that makes instantiating exceptions much much faster,
and this is only after like 15 mins of work. I'm sure there's even more to
gain.
Nice!
Marco Pivetta
Regarding performance however, rather than introducing yet another completely new concept to do almost the same thing, why not try to improve exception performance instead?
I just opened a PR that makes instantiating exceptions much much faster, and this is only after like 15 mins of work. I'm sure there's even more to gain.
I mean, squeeze out gains where you can where the effort:reward ratio is good, but the following is a naive but representative result on an MacBook M3 Pro:
return false 100000 times = 0.075289011001587
throw exception 100000 times = 0.11530804634094
Do we consider a difference of 0.075/100000s vs 0.115/100000s that big a deal when compared to (e.g.) establishing a database connection?
This is the naive comparison code; let me know if it's comparing the wrong things.
<?php
function returnFalse(int $k)
{
$before = microtime(true);
for ($i = 0; $i < $k; $i ++) {
returnFalse0();
}
$duration = microtime(true) - $before;
echo "return false $k times = " . $duration . PHP_EOL;
}
function returnFalse0() { return returnFalse1(); }
function returnFalse1() { return returnFalse2(); }
function returnFalse2() { return returnFalse3(); }
function returnFalse3() { return returnFalse4(); }
function returnFalse4() { return returnFalse5(); }
function returnFalse5() { return returnFalse6(); }
function returnFalse6() { return returnFalse7(); }
function returnFalse7() { return returnFalse8(); }
function returnFalse8() { return returnFalse9(); }
function returnFalse9() { return false; }
function throwException(int $k)
{
$before = microtime(true);
for ($i = 0; $i < $k; $i ++) {
try {
throwException0();
} catch (Exception $e) {
}
}
$duration = microtime(true) - $before;
echo "throw exception $k times = " . $duration . PHP_EOL;
}
function throwException0() { throwException1(); }
function throwException1() { throwException2(); }
function throwException2() { throwException3(); }
function throwException3() { throwException4(); }
function throwException4() { throwException5(); }
function throwException5() { throwException6(); }
function throwException6() { throwException7(); }
function throwException7() { throwException8(); }
function throwException8() { throwException9(); }
function throwException9() { throw new Exception(); }
$k = 100000;
returnFalse($k);
throwException($k);
-- pmj
Regarding performance however, rather than introducing yet another completely new concept to do almost the same thing, why not try to improve exception performance instead?
I just opened a PR that makes instantiating exceptions much much faster, and this is only after like 15 mins of work. I'm sure there's even more to gain.
I mean, squeeze out gains where you can where the effort:reward ratio is good, but the following is a naive but representative result on an MacBook M3 Pro:
return false 100000 times = 0.075289011001587 throw exception 100000 times = 0.11530804634094
Do we consider a difference of 0.075/100000s vs 0.115/100000s that big a deal when compared to (e.g.) establishing a database connection?
The first part, the 0.075 vs 0.115... yeah, we care. That's 50%
slower. But the thing is, the math with exceptions is kind of knowable
because one of the key aspects of its cost is walking the call stack.
How deep is the call stack going to be when a given library throws an
exception? You don't really know.
I work for an observability company on a profiler, so I regularly see
customer call stacks. It is incredibly common to see much deeper call
stacks, especially any framework with a middleware concept. I can't
share a lot more detail without customer permission, but we do have a
blog post about a real-world situation for one of our customers which
had 85-ish frames deep when they were throwing exceptions:
https://www.datadoghq.com/blog/php-exception-profiling/.
tl;dr For this customer, throwing exceptions accounted for about 23%
of the total CPU time.
So yeah, we do care about the performance of exceptions. Granted,
code for this customer was bit of an unusual situation, but it still
matters even today. It definitely matters if people are going to start
throwing exceptions more frequently for less-exceptional errors.
Do we consider a difference of 0.075/100000s vs 0.115/100000s that big a deal when compared to (e.g.) establishing a database connection?
The first part, the 0.075 vs 0.115... yeah, we care. That's 50%
slower. But the thing is, the math with exceptions is kind of knowable
because one of the key aspects of its cost is walking the call stack.
How deep is the call stack going to be when a given library throws an
exception? You don't really know.
Oops, key typo: kind of unknowable.
Hi all,
Regarding performance however, rather than introducing yet another completely new concept to do almost the same thing, why not try to improve exception performance instead?
I just opened a PR that makes instantiating exceptions much much faster, and this is only after like 15 mins of work. I'm sure there's even more to gain.
I mean, squeeze out gains where you can where the effort:reward ratio is good, but the following is a naive but representative result on an MacBook M3 Pro:
return false 100000 times = 0.075289011001587 throw exception 100000 times = 0.11530804634094
Do we consider a difference of 0.075/100000s vs 0.115/100000s that big a deal when compared to (e.g.) establishing a database connection?
The first part, the 0.075 vs 0.115... yeah, we care. That's 50%
slower.
(/me nods along) A 50% increase in execution time, but a 50% increase in things that when all combined take up (e.g.) only 0.01% of the total execution time -- is that something we care about?
And "care about" relative to what else in comparison? Something like a starting a database connection might incur a much greater penalty, drowning out the exception penalty in total execution time.
But then again ...
But the thing is, the math with exceptions is kind of [un]knowable
because one of the key aspects of its cost is walking the call stack.
How deep is the call stack going to be when a given library throws an
exception? You don't really know.
[edited to include your note about the typo]
... yeah, that makes it tough to determine how much of a penalty there might be. How can we quantify that, if at all?
In any case, if there are performance gains to be squeezed out of the existing exceptions model (especially at a good effort:reward ratio) then of course that's something to pursue.
-- pmj