[PHP-DEV] Concept: Lightweight error channels

On Mon, Apr 28, 2025, at 6:46 AM, Edmond Dantes wrote:

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:

  1. 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.

On Mon, Apr 28, 2025, at 11:28 AM, Edmond Dantes 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:
1. 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.

2. 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:

On Mon, Apr 28, 2025, at 11:28 AM, Edmond Dantes 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:

  1. 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.

  1. 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

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

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

On Apr 27, 2025, at 07:26, Niels Dossche <dossche.niels@gmail.com> wrote:

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

On 2025-04-28 06:06, Larry Garfield wrote:

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).

On 2025-04-29 17:29, Matthew Weier O’Phinney wrote:

  • 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

On Wed, Apr 30, 2025, at 9:18 PM, Morgan wrote:

On 2025-04-28 06:06, Larry Garfield wrote:

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

On Thu, May 1, 2025, at 7:47 AM, Juris Evertovskis wrote:

On 2025-04-29 17:29, Matthew Weier O'Phinney wrote:

* 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

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:

1. Do more than "superficial validation" at the higher level. Validate field types *and* that a user is authorized to write this value (for example).
2. 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. :slight_smile: 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

On Tue, Apr 29, 2025 at 9:48 AM Paul M. Jones <pmjones@pmjones.io> wrote:

> On Apr 27, 2025, at 07:26, Niels Dossche <dossche.niels@gmail.com> wrote:
>
> 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:
Why care about exception profiling in PHP? | Datadog.

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,

On May 1, 2025, at 09:29, Levi Morrison <levi.morrison@datadoghq.com> wrote:

On Tue, Apr 29, 2025 at 9:48 AM Paul M. Jones <pmjones@pmjones.io> wrote:

On Apr 27, 2025, at 07:26, Niels Dossche <dossche.niels@gmail.com> wrote:

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

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. :slight_smile:

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.