Hello,
I would like to introduce a new stream error handling RFC that is part of
my stream evolution work (PHP Foundation project funded by Sovereign Tech
Fund) :
https://wiki.php.net/rfc/stream_errors
Kind regards,
Jakub
Hello,
I would like to introduce a new stream error handling RFC that is part of
my stream evolution work (PHP Foundation project funded by Sovereign Tech
Fund) :https://wiki.php.net/rfc/stream_errors
Kind regards,
Jakub
Hi Jakub,
This is a nice RFC. Since I use a lot of PHP streaming capabilities I am
looking forward to its voting outcome. Having said that, did you think,
instead adding more constants in the global namespace, of using Enum as an
alternative for error code. There is a precedent for that in the new URI
extension, This would
reduce the number of new constants added and perhaps make for a more modern
DX for the feature.
What do you think?
Best regards,
Ignace
Hey Jakub,
Hello,
I would like to introduce a new stream error handling RFC that is part
of my stream evolution work (PHP Foundation project funded by
Sovereign Tech Fund) :https://wiki.php.net/rfc/stream_errors
Kind regards,
Jakub
I super appreciate that you've tackled this. This has been a big wart in
PHP's stream handling for a long time.
I have a couple questions:
Why have you chosen a big list of constants rather than enums?
I think we should push for enums where applicable, meaning enum
StreamErrorMode, enum StreamErrorStore, enum StreamError.
You define what a terminal error is, but does it correlate to specific
error codes? Like, is a given error code always terminal or not
terminal? Or can some error codes sometimes be terminal or not terminal
depending on context?
I've tried to look at the code, but I don't quite understand it, e.g.
php_plain_files_rename: When chown or chmod fail, the error is terminal.
When the copying fails (did you forget to adapt php_copy_file_ctx?),
that's non-terminal, though. Reading through the code, I have the
feeling that operations which do not call out to other operations, which
can error, are terminal. And all others are non-terminal. E.g. failing
to open a file is terminal. That the copying where the file opening is
part of, failed, is non-terminal.
And then additionally some operations, which don't prevent success are
also marked non-terminal - squeezing it into the boolean for the purpose
of not erroring.
Which makes some sense to me, but the description in the RFC is really
confusing here. And the meanings are muddied.
Thus, if I understand that correctly, there should be an unique mapping
from error code to terminal. If we were to go with enums, we could make
it trivially a property of the individual enum values instead.
Should StreamException be attached the non-terminal errors which are
caused by the terminal errors? I.e. StreamException clearly is
STREAM_ERROR_CODE_PERMISSION_DENIED for example, but it happens during a
copy, so, should the information that a STREAM_ERROR_CODE_COPY_FAILED
was caused by that, also be present on the StreamException?
Are stream errors which happen as part of a fully wrapped functionality
marked as wrapper error as well? Like if the server sent an invalid TLS
response to a file_get_contents("https://...") operation, making the
whole operation fail? You obviously don't have access to the
intermediary internal stream resource there, where the stream error is
attached.
Is this what the logged errors section is about? Is that section talking
about "php_stream_display_wrapper_errors", which can concat some errors?
I see that this logic is mostly retained.
Do I understand the implementation correctly, that user error handlers
can be called multiple times for a same operation? E.g. when copying a
file, you'll get first a PERMISSION_DENIED then a COPY_FAILED? Should
these not be somehow merged? Especially with global user_error handlers,
these cannot be aware of the fact that the permission denied is part of
anything else - at least not until possibly a second user_error
invocation happens.
Also, is the internal resource used for e.g. file_get_contents() leaked
into user error handlers? I'm not sure whether that's desirable. If
that's intended, that's fine too, I think.
Further suggestion on stream_get_errors() - why not return a proper
object instead of a loosely typed array? Proper typing, and creation is
actually faster. Also "docref" is so... antiquated? maybe just
"function" and have it contain the function name of the current operation.
Does stream_get_errors() ever reset? Looking at the code it doesn't seem
so. It will just forever grow. E.g. fwrite() will report an error every
single time when the buffer is full and a write is attempted. If errors
aren't freed, that's just a memory leak (as long as the socket lives).
This is a serious flaw.
Lastly, stream_get_errors() is overkill in a lot of cases (especially
when using fread()/fwrite() on simple sockets). I just care about the
last error code (there's just going to be one anyway). I'd love having a
stream_get_last_error() returning just the code.
Overall, I like the direction of the RFC, but I'd like to see improved
handling for "terminal error inside non-terminal operation having its
own error" in particular. Enums are rather a suggestion, not a
requirement, but they would be in my opinion very fitting here.
Thanks,
Bob
Hi Bob,
Thanks for the feedback, the replies here also cover the feedback from
Jordi and Ignace.
Why have you chosen a big list of constants rather than enums?
I think we should push for enums where applicable, meaning enum
StreamErrorMode, enum StreamErrorStore, enum StreamError.
Ok I will change it. StreamErrorMode and StreamErrorStore should be
straightforward. The code one should be probably named StreamErrorCode (to
not interact with what Jordi proposed) and think it will need to be backed
as there should be a straightforward way how to compare it with exception
code. I might need to extend the gen stub as I'm not sure it allows macro
import for enum value like for constants or I will need to come up with
some mapping. I just don't wan to duplicate the numbers. I will figure
something out.
You define what a terminal error is, but does it correlate to specific
error codes? Like, is a given error code always terminal or not
terminal? Or can some error codes sometimes be terminal or not terminal
depending on context?
I've tried to look at the code, but I don't quite understand it, e.g.
php_plain_files_rename: When chown or chmod fail, the error is terminal.
When the copying fails (did you forget to adapt php_copy_file_ctx?),
that's non-terminal, though. Reading through the code, I have the
feeling that operations which do not call out to other operations, which
can error, are terminal. And all others are non-terminal. E.g. failing
to open a file is terminal. That the copying where the file opening is
part of, failed, is non-terminal.
And then additionally some operations, which don't prevent success are
also marked non-terminal - squeezing it into the boolean for the purpose
of not erroring.
Which makes some sense to me, but the description in the RFC is really
confusing here. And the meanings are muddied.
So currently the terminal / non terminal does not attach to code but it
could be done. The current logic is really just supposed to reflect what is
there already so the exception can be thrown in places where the function
would fail. So the errors after which the function fails are terminal and
the errors where the function still returns success are non terminal.
Thus, if I understand that correctly, there should be an unique mapping
from error code to terminal. If we were to go with enums, we could make
it trivially a property of the individual enum values instead.
I think making it property of enum make more sense. Then I can drop that
boolean property.
Should StreamException be attached the non-terminal errors which are
caused by the terminal errors? I.e. StreamException clearly is
STREAM_ERROR_CODE_PERMISSION_DENIED for example, but it happens during a
copy, so, should the information that a STREAM_ERROR_CODE_COPY_FAILED
was caused by that, also be present on the StreamException?
I think I get what you mean from the user point of view but I'm not sure
how could this be reasonably linked.
I could maybe check during stream error if there is EG(exception) it's a
StreamException and if so, I could the update it with this error (e.g. some
property that would hold those error can could be retrieved). I guess it
could be potentially extended for terminal errors as well so it could have
method like getAllErrors which would return all errors because sometimes
there can be more terminal errors and some of them might get lost (last one
will be added). However this would work just for exception so maybe more
generic solution is needed here (see below).
Are stream errors which happen as part of a fully wrapped functionality
marked as wrapper error as well? Like if the server sent an invalid TLS
response to a file_get_contents("https://...") operation, making the
whole operation fail? You obviously don't have access to the
intermediary internal stream resource there, where the stream error is
attached.
I actually thought about this yesterday that it's not ideal that some
errors might get lost in this way so I think it should go to wrapper errors
as well.
Is this what the logged errors section is about? Is that section talking
about "php_stream_display_wrapper_errors", which can concat some errors?
I see that this logic is mostly retained.
Yeah the logged errors is an existing mechanism for grouping errors to a
single one (concatenating message). The display is a bit messy because it
separated them by new line or <br/> (if html errors enabled) but I just
kept it to limit BC impact.
Do I understand the implementation correctly, that user error handlers
can be called multiple times for a same operation? E.g. when copying a
file, you'll get first a PERMISSION_DENIED then a COPY_FAILED? Should
these not be somehow merged? Especially with global user_error handlers,
these cannot be aware of the fact that the permission denied is part of
anything else - at least not until possibly a second user_error
invocation happens.
This is somehow related to the exception error grouping as it requires some
sort of linking between the errors.
I guess to make this work we would need some top level wrapping similar to
what logged errors do but instead of concatenating, it would group the
errors in a better way which could then be used in exception or handler.
This would, however, increase the complexity significantly as it is
possible to nest the stream calls (e.g. stream calls in notification
callback or in the new error handler) so there would probably need to be
some stack. Also I'm not sure about the API. Should we pass always array of
errors to the callback (which would mostly have just one element anyway or
somehow chain the StreamError object (something like previousError()) and
what message should be used in exception (e.g. last error and then helper
function to get all errors)?
I'm willing to look into this but would like to know if this is something
important for user space and what is the usual flow how could this be used
in frameworks and so on. In other words I would like to get more input to
make sure that this is really needed as it requires quite a bit of effort
to do.
Also, is the internal resource used for e.g.
file_get_contents()leaked
into user error handlers? I'm not sure whether that's desirable. If
that's intended, that's fine too, I think.
Currently yes. I need to actually double check the consequences of that as
I didn't really think about internal resources.
Further suggestion on stream_get_errors() - why not return a proper
object instead of a loosely typed array? Proper typing, and creation is
actually faster. Also "docref" is so... antiquated? maybe just
"function" and have it contain the function name of the current operation.
Yeah I will go for it and introduce StreamError as Jordi suggested with
slight modification to use enum and dropping terminal and docref.
Does stream_get_errors() ever reset? Looking at the code it doesn't seem
so. It will just forever grow. E.g.fwrite()will report an error every
single time when the buffer is full and a write is attempted. If errors
aren't freed, that's just a memory leak (as long as the socket lives).
This is a serious flaw.Lastly, stream_get_errors() is overkill in a lot of cases (especially
when usingfread()/fwrite() on simple sockets). I just care about the
last error code (there's just going to be one anyway). I'd love having a
stream_get_last_error() returning just the code.
I guess this makes more sense but it kind of also implies the top level
wrapping as it should contain all errors in the call. Or alternatively it
could be just a ring with limited number of errors be behave in the same
way like openssl_error_string. I guess it depends if we go with that top
level wrapping.
Cheers,
Jakub
Hey Jakub,
thanks for the detailed reply.
The code one should be probably named StreamErrorCode (to not interact
with what Jordi proposed) and think it will need to be backed as there
should be a straightforward way how to compare it with exception code.
I might need to extend the gen stub as I'm not sure it allows macro
import for enum value like for constants or I will need to come up
with some mapping. I just don't wan to duplicate the numbers. I will
figure something out.
How important is it to have exception code?
I found that for our purposes we generally skip the exception code (i.e.
keep it at zero).
As a simple example, ErrorException does take two integers - one for
severity, and one for code, instead of using the severity as code.
I would propose something similar here: call it StreamError and provide
a public $error property for the actual enum, rather than squeezing it
into $code.
This also solves your gen-stub issues by making them unncessary.
Should StreamException be attached the non-terminal errors which are caused by the terminal errors? I.e. StreamException clearly is STREAM_ERROR_CODE_PERMISSION_DENIED for example, but it happens during a copy, so, should the information that a STREAM_ERROR_CODE_COPY_FAILED was caused by that, also be present on the StreamException?I think I get what you mean from the user point of view but I'm not
sure how could this be reasonably linked.I could maybe check during stream error if there is EG(exception) it's
a StreamException and if so, I could the update it with this error
(e.g. some property that would hold those error can could be
retrieved). I guess it could be potentially extended for terminal
errors as well so it could have method like getAllErrors which would
return all errors because sometimes there can be more terminal errors
and some of them might get lost (last one will be added). However this
would work just for exception so maybe more generic solution is needed
here (see below).
Replying below to this.
Are stream errors which happen as part of a fully wrapped functionality marked as wrapper error as well? Like if the server sent an invalid TLS response to a file_get_contents("https://...") operation, making the whole operation fail? You obviously don't have access to the intermediary internal stream resource there, where the stream error is attached.I actually thought about this yesterday that it's not ideal that some
errors might get lost in this way so I think it should go to wrapper
errors as well.
If we properly wrap the nested operations into a single error, then it
would be automatically part of the wrapper error, with no need to
explicitly add it to stream error.
Is this what the logged errors section is about? Is that section talking about "php_stream_display_wrapper_errors", which can concat some errors? I see that this logic is mostly retained.Yeah the logged errors is an existing mechanism for grouping errors to
a single one (concatenating message). The display is a bit messy
because it separated them by new line or <br/> (if html errors
enabled) but I just kept it to limit BC impact.
I don't think changing the display / grouping here would be problematic
with respect to BC. But I get that this initial draft tries to limit
what it changes.
Do I understand the implementation correctly, that user error handlers can be called multiple times for a same operation? E.g. when copying a file, you'll get first a PERMISSION_DENIED then a COPY_FAILED? Should these not be somehow merged? Especially with global user_error handlers, these cannot be aware of the fact that the permission denied is part of anything else - at least not until possibly a second user_error invocation happens.This is somehow related to the exception error grouping as it requires
some sort of linking between the errors.I guess to make this work we would need some top level wrapping
similar to what logged errors do but instead of concatenating, it
would group the errors in a better way which could then be used in
exception or handler. This would, however, increase the complexity
significantly as it is possible to nest the stream calls (e.g. stream
calls in notification callback or in the new error handler) so there
would probably need to be some stack. Also I'm not sure about the API.
Should we pass always array of errors to the callback (which would
mostly have just one element anyway or somehow chain the StreamError
object (something like previousError()) and what message should be
used in exception (e.g. last error and then helper function to get all
errors)?I'm willing to look into this but would like to know if this is
something important for user space and what is the usual flow how
could this be used in frameworks and so on. In other words I would
like to get more input to make sure that this is really needed as it
requires quite a bit of effort to do.
Doing this work will definitely tidy up the whole error story. It's not
fundamentally crucial, but it definitely will make for a nicer API.
And yes, chaining StreamError sounds and nesting exceptions to $previous
sounds like the most straightforward way.
Also a possibility for exceptions: this might be a bit of work, but you
could insert a fake frame for exceptions to indicate when an error
appears as a sub-operation. Like:
StreamException: Permission denied
#0 [internal function]: fopen("target.php", "w+")
#1 [internal function]: copy("source.php", "target.php")
#2 ...
I also like the suggestion by Jordi to pass the StreamError right away
into the user handler.
Also, is the internal resource used for e.g. `file_get_contents()` leaked into user error handlers? I'm not sure whether that's desirable. If that's intended, that's fine too, I think.Currently yes. I need to actually double check the consequences of
that as I didn't really think about internal resources.
If we decide to go with completely wrapping and calling the user error
handler only once per top-level operation, this point would be moot anyway.
Does stream_get_errors() ever reset? Looking at the code it doesn't seem so. It will just forever grow. E.g. `fwrite()` will report an error every single time when the buffer is full and a write is attempted. If errors aren't freed, that's just a memory leak (as long as the socket lives). This is a serious flaw. Lastly, stream_get_errors() is overkill in a lot of cases (especially when using `fread()`/fwrite() on simple sockets). I just care about the last error code (there's just going to be one anyway). I'd love having a stream_get_last_error() returning just the code.I guess this makes more sense but it kind of also implies the top
level wrapping as it should contain all errors in the call. Or
alternatively it could be just a ring with limited number of errors be
behave in the same way like openssl_error_string. I guess it depends
if we go with that top level wrapping.
I feel like the top-level wrapping would simplify things a lot (from
point of addressing shortcomings), and if this just works like
json_get_last_error(), where you only ever see the last error, there's
no risks of being unbounded nor having arbitrary size limits.
The only disadvantage of top-level wrapping is probably a bit more
book-keeping as in "storing that there is an outer operation before
invoking an inner operation"? Haven't thought much about the actual
implementation of this, but should be doable.
Thanks,
Bob
Hi Bob,
Hey Jakub,
thanks for the detailed reply.
The code one should be probably named StreamErrorCode (to not interact
with what Jordi proposed) and think it will need to be backed as there
should be a straightforward way how to compare it with exception code. I
might need to extend the gen stub as I'm not sure it allows macro import
for enum value like for constants or I will need to come up with some
mapping. I just don't wan to duplicate the numbers. I will figure something
out.How important is it to have exception code?
I think the exception code could be quite useful for checking for specific
errors. It's much better than trying to match it by error message which I
saw in past used in normal error handlers. So I can see definitely use case
for that. It might be also possible to group those erors if enums are used
so users could just match specific group of errors.
I found that for our purposes we generally skip the exception code (i.e.
keep it at zero).
As a simple example, ErrorException does take two integers - one for
severity, and one for code, instead of using the severity as code.
Severity is almost always warning which I don't think is a good
representation for code. I saw error codes used in various application and
I think they are useful. I don't think it hurts to have them. The
implementation of that enum should not be a big problem. I just need to
figure out how to make it nice.
I would propose something similar here: call it StreamError and provide a
public $error property for the actual enum, rather than squeezing it into
$code.
I'm not sure I understand this. The code is a generic identifier for the
error class that can be matched. StreamError would be the actual
representation containing the message and other info - this cannot be
matched in a generic way.
Is this what the logged errors section is about? Is that section talking
about "php_stream_display_wrapper_errors", which can concat some errors?
I see that this logic is mostly retained.Yeah the logged errors is an existing mechanism for grouping errors to a
single one (concatenating message). The display is a bit messy because it
separated them by new line or <br/> (if html errors enabled) but I just
kept it to limit BC impact.I don't think changing the display / grouping here would be problematic
with respect to BC. But I get that this initial draft tries to limit what
it changes.
Exactly I would like to limit the external changes.
Do I understand the implementation correctly, that user error handlers
can be called multiple times for a same operation? E.g. when copying a
file, you'll get first a PERMISSION_DENIED then a COPY_FAILED? Should
these not be somehow merged? Especially with global user_error handlers,
these cannot be aware of the fact that the permission denied is part of
anything else - at least not until possibly a second user_error
invocation happens.This is somehow related to the exception error grouping as it requires
some sort of linking between the errors.I guess to make this work we would need some top level wrapping similar to
what logged errors do but instead of concatenating, it would group the
errors in a better way which could then be used in exception or handler.
This would, however, increase the complexity significantly as it is
possible to nest the stream calls (e.g. stream calls in notification
callback or in the new error handler) so there would probably need to be
some stack. Also I'm not sure about the API. Should we pass always array of
errors to the callback (which would mostly have just one element anyway or
somehow chain the StreamError object (something like previousError()) and
what message should be used in exception (e.g. last error and then helper
function to get all errors)?I'm willing to look into this but would like to know if this is something
important for user space and what is the usual flow how could this be used
in frameworks and so on. In other words I would like to get more input to
make sure that this is really needed as it requires quite a bit of effort
to do.Doing this work will definitely tidy up the whole error story. It's not
fundamentally crucial, but it definitely will make for a nicer API.And yes, chaining StreamError sounds and nesting exceptions to $previous
sounds like the most straightforward way.Also a possibility for exceptions: this might be a bit of work, but you
could insert a fake frame for exceptions to indicate when an error appears
as a sub-operation. Like:StreamException: Permission denied
#0 [internal function]: fopen("target.php", "w+")
#1 [internal function]: copy("source.php", "target.php")
#2 ...I will check this out.
I also like the suggestion by Jordi to pass the StreamError right away
into the user handler.Yeah agreed.
Also, is the internal resource used for e.g.
file_get_contents()leakedinto user error handlers? I'm not sure whether that's desirable. If
that's intended, that's fine too, I think.Currently yes. I need to actually double check the consequences of that as
I didn't really think about internal resources.If we decide to go with completely wrapping and calling the user error
handler only once per top-level operation, this point would be moot anyway.Does stream_get_errors() ever reset? Looking at the code it doesn't seem
so. It will just forever grow. E.g.
fwrite()will report an error every
single time when the buffer is full and a write is attempted. If errors
aren't freed, that's just a memory leak (as long as the socket lives).
This is a serious flaw.Lastly, stream_get_errors() is overkill in a lot of cases (especially
when usingfread()/fwrite() on simple sockets). I just care about the
last error code (there's just going to be one anyway). I'd love having a
stream_get_last_error() returning just the code.I guess this makes more sense but it kind of also implies the top level
wrapping as it should contain all errors in the call. Or alternatively it
could be just a ring with limited number of errors be behave in the same
way like openssl_error_string. I guess it depends if we go with that top
level wrapping.I feel like the top-level wrapping would simplify things a lot (from point
of addressing shortcomings), and if this just works like
json_get_last_error(), where you only ever see the last error, there's no
risks of being unbounded nor having arbitrary size limits.
Yeah I think it would be more logical and improve things so I will check it
out and see when I have my next stream errors slot (should be later next
month).
Cheers,
Jakub
Hey Jakub,
The code one should be probably named StreamErrorCode (to not interact with what Jordi proposed) and think it will need to be backed as there should be a straightforward way how to compare it with exception code. I might need to extend the gen stub as I'm not sure it allows macro import for enum value like for constants or I will need to come up with some mapping. I just don't wan to duplicate the numbers. I will figure something out.How important is it to have exception code?I think the exception code could be quite useful for checking for
specific errors. It's much better than trying to match it by error
message which I saw in past used in normal error handlers. So I can
see definitely use case for that. It might be also possible to group
those erors if enums are used so users could just match specific group
of errors.
For the purpose of grouping it would be vastly preferable to add
functions to the enum class to mark enum values as pertaining to
specific groups "function isIoError(): bool", "function
isFileSystemError(): bool" etc. or just simply "function errorGroup():
StreamErrorGroup" which is another enum with some grooups like "Io",
"Filesystem" etc. rather than comparing ranges.
I found that for our purposes we generally skip the exception code (i.e. keep it at zero). As a simple example, ErrorException does take two integers - one for severity, and one for code, instead of using the severity as code.Severity is almost always warning which I don't think is a good
representation for code. I saw error codes used in various application
and I think they are useful. I don't think it hurts to have them. The
implementation of that enum should not be a big problem. I just need
to figure out how to make it nice.I would propose something similar here: call it StreamError and provide a public $error property for the actual enum, rather than squeezing it into $code.I'm not sure I understand this. The code is a generic identifier for
the error class that can be matched. StreamError would be the actual
representation containing the message and other info - this cannot be
matched in a generic way.
My point here is that $code generally does not get used for our
Exceptions. Whenever we need to attach extra information to an
exception, we do it as extraneous properties. (That's what my example
with ErrorException was about.)
I.e. ignore the existence of $code (just assign zero).
And add an extra property for the StreamError enum (which now needs no
backing).
class StreamException extends Exception {
public __construct(
public StreamError $error,
public string $wrapperName,
string $message = "",
public string|null $param = "",
?Throwable $previous = null
) {
parent::__construct($message, 0, $previous);
}
}
Does this make more sense, described as code?
Thank you,
Bob
Hi Bob,
Hey Jakub,
The code one should be probably named StreamErrorCode (to not interact
with what Jordi proposed) and think it will need to be backed as there
should be a straightforward way how to compare it with exception code. I
might need to extend the gen stub as I'm not sure it allows macro import
for enum value like for constants or I will need to come up with some
mapping. I just don't wan to duplicate the numbers. I will figure something
out.How important is it to have exception code?
I think the exception code could be quite useful for checking for specific
errors. It's much better than trying to match it by error message which I
saw in past used in normal error handlers. So I can see definitely use case
for that. It might be also possible to group those erors if enums are used
so users could just match specific group of errors.For the purpose of grouping it would be vastly preferable to add functions
to the enum class to mark enum values as pertaining to specific groups
"function isIoError(): bool", "function isFileSystemError(): bool" etc. or
just simply "function errorGroup(): StreamErrorGroup" which is another enum
with some grooups like "Io", "Filesystem" etc. rather than comparing ranges.Yeah that's what I was thinking. Internally I would still probably
represent it as num ranges (as it is now) but userspace should use those
enum functions.
I found that for our purposes we generally skip the exception code (i.e.
keep it at zero).
As a simple example, ErrorException does take two integers - one for
severity, and one for code, instead of using the severity as code.Severity is almost always warning which I don't think is a good
representation for code. I saw error codes used in various application and
I think they are useful. I don't think it hurts to have them. The
implementation of that enum should not be a big problem. I just need to
figure out how to make it nice.I would propose something similar here: call it StreamError and provide a
public $error property for the actual enum, rather than squeezing it into
$code.I'm not sure I understand this. The code is a generic identifier for the
error class that can be matched. StreamError would be the actual
representation containing the message and other info - this cannot be
matched in a generic way.My point here is that $code generally does not get used for our
Exceptions. Whenever we need to attach extra information to an exception,
we do it as extraneous properties. (That's what my example with
ErrorException was about.)I.e. ignore the existence of $code (just assign zero).
And add an extra property for the StreamError enum (which now needs no
backing).class StreamException extends Exception {
public __construct(
public StreamError $error,
public string $wrapperName,
string $message = "",
public string|null $param = "",
?Throwable $previous = null
) {
parent::__construct($message, 0, $previous);
}
}Does this make more sense, described as code?
As I see what you mean now. I would prefer to use StreamError name for the
class holding all the details including code, wrapper name, message and
param as proposed by Jordi because this could be also passed to handler and
retrieved from the stored errors. It just seems not right to use
StreamException as a container without being thrown (is that done
anywhere?) so I think separate class for handler and stored errors (or
single last error) seems cleaner. Although StreamException would be then
quite similar. I could pass StreamError there as a property but then it
would duplicate the message so not sure.
In any case I think it's better to call that enum StreamErrorCode. I will
still find a clean way to map it to the defines (STREAM_ERROR_CODE_*
macros) because that's what I use internally to set code in the error. So
not using int value for the exception code won't make that much difference
from the implementation PoV.
Kind regards,
Jakub
Hey Jakub,
My point here is that $code generally does not get used for our Exceptions. Whenever we need to attach extra information to an exception, we do it as extraneous properties. (That's what my example with ErrorException was about.) I.e. ignore the existence of $code (just assign zero). And add an extra property for the StreamError enum (which now needs no backing). class StreamException extends Exception { public __construct( public StreamError $error, public string $wrapperName, string $message = "", public string|null $param = "", ?Throwable $previous = null ) { parent::__construct($message, 0, $previous); } } Does this make more sense, described as code?As I see what you mean now. I would prefer to use StreamError name for
the class holding all the details including code, wrapper name,
message and param as proposed by Jordi because this could be also
passed to handler and retrieved from the stored errors. It just seems
not right to use StreamException as a container without being thrown
(is that done anywhere?) so I think separate class for handler and
stored errors (or single last error) seems cleaner. Although
StreamException would be then quite similar. I could pass StreamError
there as a property but then it would duplicate the message so not sure.In any case I think it's better to call that enum StreamErrorCode. I
will still find a clean way to map it to the defines
(STREAM_ERROR_CODE_* macros) because that's what I use internally to
set code in the error. So not using int value for the exception code
won't make that much difference from the implementation PoV.
I don't mind the name, whether you call it StreamError or
StreamErrorCode. Or maybe call the class "StreamFailure" and the enum
"StreamError"? Just a suggestion, I don't care too much about the actual
names.
For the internal implementation, does it actually matter passing around
an int value or a char pointer with the name? Which would allow you to
directly pass it into the enum lookup. But yup, do whatever fits you
best here :-)
(Ranty tangent: Umm, what's even the internal proper way to get an enum
value? Manual zend_hash_find_ptr(CE_CONSTANTS_TABLE(ce), constant_name)
including zend_update_class_constant()?! We ought to figure a way to
create the internal enum case objects at compile time, then we could
just reference them directly... Working with enums internally can be
annoying and fetching feels slow (even if it's just a HashTable lookup).)
Bob
Hey Jakub,
Very nice improvement overall. Beyond the enum comment from Bob/Ignace
I would also think that stream_get_errors() could return an array of
StreamError objects instead of arrays?
readonly class StreamError
{
public function __construct(
public string $message,
public int $code,
public int $severity,
public bool $terminal,
public string $wrapper,
public ?string $param = null,
public ?string $docref = null,
) {}
}
This object could also be passed to the error handlers perhaps as:
function(string $wrapper, ?resource $stream, StreamError $error): void
Just some ideas :)
Best,
Jordi
Hello,
I would like to introduce a new stream error handling RFC that is part of
my stream evolution work (PHP Foundation project funded by Sovereign Tech
Fund) :https://wiki.php.net/rfc/stream_errors
Kind regards,
Jakub
The naming of "terminal" is a little confusing to me. Reading the name with
a bool made me think it was whether or not the CLI was being used, which
made little sense in the context of stream errors. After reading the
explanation "Terminal errors are those that prevent an operation from
completing (e.g., file not found, permission denied). Non-terminal errors
are warnings or notices that don't stop execution (e.g., buffer
truncation).", I feel like it might be more clear if this property was
named after what the result is, perhaps something like "operationBlocked"
or "operationPrevented".
Hello,
I would like to introduce a new stream error handling RFC that is part of
my stream evolution work (PHP Foundation project funded by Sovereign Tech
Fund) :https://wiki.php.net/rfc/stream_errors
Kind regards,
Jakub
The naming of "terminal" is a little confusing to me. Reading the name
with a bool made me think it was whether or not the CLI was being used,
which made little sense in the context of stream errors. After reading the
explanation "Terminal errors are those that prevent an operation from
completing (e.g., file not found, permission denied). Non-terminal errors
are warnings or notices that don't stop execution (e.g., buffer
truncation).", I feel like it might be more clear if this property was
named after what the result is, perhaps something like "operationBlocked"
or "operationPrevented".
I believe these types of issues can be resolved by replacing boolean values
by Enums, hence my suggestion. Enum would be more self explanatory and
would provide better insight on the outcome. I also like Jordi's
suggestion, anything that can help static analysis is the way to go IMHO.
Best regards,
Ignace
On Wed, Nov 19, 2025 at 10:48 AM ignace nyamagana butera <
nyamsprod@gmail.com> wrote:
Hello,
I would like to introduce a new stream error handling RFC that is part
of my stream evolution work (PHP Foundation project funded by Sovereign
Tech Fund) :https://wiki.php.net/rfc/stream_errors
Kind regards,
Jakub
The naming of "terminal" is a little confusing to me. Reading the name
with a bool made me think it was whether or not the CLI was being used,
which made little sense in the context of stream errors. After reading the
explanation "Terminal errors are those that prevent an operation from
completing (e.g., file not found, permission denied). Non-terminal errors
are warnings or notices that don't stop execution (e.g., buffer
truncation).", I feel like it might be more clear if this property was
named after what the result is, perhaps something like "operationBlocked"
or "operationPrevented".I believe these types of issues can be resolved by replacing boolean
values by Enums, hence my suggestion. Enum would be more self explanatory
and would provide better insight on the outcome. I also like Jordi's
suggestion, anything that can help static analysis is the way to go IMHO.Best regards,
Ignace
I ended up removing this from my first reply, but an enum was also what I
thought of as it wouldn't surprise me if a 3rd state could be introduced:
fully prevented, fully executed but with warnings, and partially executed
like a buffer truncation. I just don't know enough about the underlying
errors/warnings to suggest this as alternative implementation.
Hi,
On Wed, Nov 19, 2025 at 10:48 AM ignace nyamagana butera <
nyamsprod@gmail.com> wrote:Hello,
I would like to introduce a new stream error handling RFC that is part
of my stream evolution work (PHP Foundation project funded by Sovereign
Tech Fund) :https://wiki.php.net/rfc/stream_errors
Kind regards,
Jakub
The naming of "terminal" is a little confusing to me. Reading the name
with a bool made me think it was whether or not the CLI was being used,
which made little sense in the context of stream errors. After reading the
explanation "Terminal errors are those that prevent an operation from
completing (e.g., file not found, permission denied). Non-terminal errors
are warnings or notices that don't stop execution (e.g., buffer
truncation).", I feel like it might be more clear if this property was
named after what the result is, perhaps something like "operationBlocked"
or "operationPrevented".I believe these types of issues can be resolved by replacing boolean
values by Enums, hence my suggestion. Enum would be more self explanatory
and would provide better insight on the outcome. I also like Jordi's
suggestion, anything that can help static analysis is the way to go IMHO.Best regards,
IgnaceI ended up removing this from my first reply, but an enum was also what I
thought of as it wouldn't surprise me if a 3rd state could be introduced:
fully prevented, fully executed but with warnings, and partially executed
like a buffer truncation. I just don't know enough about the underlying
errors/warnings to suggest this as alternative implementation.
I don't think we will add another state. The enum based approach seems most
reasonable to me. I should also change the name terminal / non terminal to
terminating / non terminating as I think that's the correct name for this...
Cheers,
Jakub
Hello,
I would like to introduce a new stream error handling RFC that is part of my stream evolution work (PHP Foundation project funded by Sovereign Tech Fund) :
Hi Jakub,
Thank you for working on this, this has been a long-standing pain point of our stream layer.
However, I do have various comments about the design of this.
Echoing multiple voices, please use enums rather than constants where applicable, and use an object rather than an associative array for individual stream errors.
As others have pointed out, the notion of (non-)"terminal" errors is not clear, and I'm struggling to understand what is the point of it?
How does this interact with the stream notification stuff?
(I guess surrounding PHP_STREAM_NOTIFY_SEVERITY_INFO and PHP_STREAM_NOTIFY_SEVERITY_ERR)
Regarding "docref" what is the point of including this in the error?
This is mainly used with php_docref to generate links to INI settings by referencing an XML ID that exists in doc-en, this does not seem applicable here.
I guess you added this due to the two usages of "streams.crypto" as a docref in main/streams/transports.c, but this ID doesn't exist, and I fear this might never have worked properly.
As such, I would recommend just getting rid of this.
The last thing that I feel very strongly about is that there should only be 3 modes:
- Silent
- Warnings
- Exceptions
As such, the cases where we currently emit a notice should either be promoted to warnings (which for some of them I don't even understand why they are just notices) or removed (mainly looking at the one in main/streams/userspace.c)
And the singular case which bailouts using E_ERROR should be converted to always throw an exception.
Bailouts cause all sorts of issues due to the lack of unwinding, things that exceptions solve.
This would also simplify both the internal, and userland API and get rid of the confusing notion of "errors" meaning "diagnostics/warnings".
Best regards,
Gina P. Banyard
Hi Gina,
On Tuesday, 18 November 2025 at 18:41, Jakub Zelenka bukka@php.net
wrote:Hello,
I would like to introduce a new stream error handling RFC that is part of
my stream evolution work (PHP Foundation project funded by Sovereign Tech
Fund) :https://wiki.php.net/rfc/stream_errors
Hi Jakub,
Thank you for working on this, this has been a long-standing pain point of
our stream layer.
However, I do have various comments about the design of this.Echoing multiple voices, please use enums rather than constants where
applicable, and use an object rather than an associative array for
individual stream errors.
As others have pointed out, the notion of (non-)"terminal" errors is not
clear, and I'm struggling to understand what is the point of it?
I just answered this in replay to Bob. The non terminating errors are those
that don't change return value of the function (it means not returning
failure). Some of those errors are still useful and users should be
notified but they are not critical for correct result. This was mainly
introduced for exceptions as those cases should not throw but it still
makes sense to somehow notify users (e.g. store the error).
How does this interact with the stream notification stuff?
(I guess surrounding PHP_STREAM_NOTIFY_SEVERITY_INFO and
PHP_STREAM_NOTIFY_SEVERITY_ERR)
It doesn't interact with it. Although PHP_STREAM_NOTIFY_SEVERITY_ERR
somehow overlaps with it but it's limited in use (currently used only in
ftp and http wrapper). Obviously I couldn't just use notifications and I
needed to keep them working so the only possibility was to introduce a new
API that handles just errors. Going forward I'm not
sure PHP_STREAM_NOTIFY_SEVERITY_ERR should be kept but that's more a future
scope I guess.
Regarding "docref" what is the point of including this in the error?
This is mainly used with php_docref to generate links to INI settings by
referencing an XML ID that exists in doc-en, this does not seem applicable
here.
I guess you added this due to the two usages of "streams.crypto" as a
docref in main/streams/transports.c, but this ID doesn't exist, and I
fear this might never have worked properly.
As such, I would recommend just getting rid of this.
Agreed I'm not going to expose it. I will keep it just for error mode to
keep it the same but I guess it might be better to drop it completely in
the future.
The last thing that I feel very strongly about is that there should only
be 3 modes:
- Silent
- Warnings
- Exceptions
As such, the cases where we currently emit a notice should either be
promoted to warnings (which for some of them I don't even understand why
they are just notices) or removed (mainly looking at the one in
main/streams/userspace.c)
And the singular case which bailouts using E_ERROR should be converted to
always throw an exception.
Bailouts cause all sorts of issues due to the lack of unwinding, things
that exceptions solve.
This would also simplify both the internal, and userland API and get rid
of the confusing notion of "errors" meaning "diagnostics/warnings".
My aim is really not to touch the current logic and the error
classification except one case where reporting was done accidentally. I
think that non terminating errors make some sense as they give some hint
that something could be improved but it's not necessarily preventing the
functioning. This is something that has been already there and I don't
think it needs to be changed as part of this proposal.
Cheers,
Jakub
Hello,
I would like to introduce a new stream error handling RFC that is part
of my stream evolution work (PHP Foundation project funded by Sovereign
Tech Fund) :https://wiki.php.net/rfc/stream_errors
Kind regards,
Jakub
HI Jakub.
I agree with what others have said so far. I'm going to ask a little bit further, though.
AIUI, there's two main issues with the stream API:
- It's very inconsistent.
- It's very clunky, non-obvious, and hard to use if you don't know exactly what you're doing.
This RFC seems to be addressing the first point, which is fine. However, should we be trying to smooth out the current unpleasant API, or should the effort be put toward a more intuitive API that is smoother from the start? This RFC could be a stepping stone toward that, I'm not sure, but if so that's not clear to me.
Are there any longer term plans here for a more complete stream overhaul?
--Larry Garfield
Hi,
On Wed, Nov 19, 2025 at 5:35 PM Larry Garfield larry@garfieldtech.com
wrote:
Hello,
I would like to introduce a new stream error handling RFC that is part
of my stream evolution work (PHP Foundation project funded by Sovereign
Tech Fund) :https://wiki.php.net/rfc/stream_errors
Kind regards,
Jakub
HI Jakub.
I agree with what others have said so far. I'm going to ask a little bit
further, though.AIUI, there's two main issues with the stream API:
- It's very inconsistent.
- It's very clunky, non-obvious, and hard to use if you don't know
exactly what you're doing.This RFC seems to be addressing the first point, which is fine. However,
should we be trying to smooth out the current unpleasant API, or should the
effort be put toward a more intuitive API that is smoother from the start?
This RFC could be a stepping stone toward that, I'm not sure, but if so
that's not clear to me.Are there any longer term plans here for a more complete stream overhaul?
This RFC is part of the bigger effort to address various stream issues and
provide other stream related improvements that I selected based on my TODO
list. You can see more detailed description of the whole scope in this
foundation blog:
https://thephp.foundation/blog/2025/10/30/php-streams-evolution/ .
The overhaul of the whole API is out of scope as there are lots of unknowns
and would require longer migration. It is thus harder to deliver in the
selected time frame. It doesn't mean that it won't happen. It's just out of
scope for this block of work and I don't have specific plans for it.
Kind regards,
Jakub
Hi Jakub,
Hello,
I would like to introduce a new stream error handling RFC that is part of my stream evolution work (PHP Foundation project funded by Sovereign Tech Fund) :
https://wiki.php.net/rfc/stream_errors
Kind regards,
Jakub
Thank you for working on this.
The RFC doesn't specify when exactly the error_handler is invoked
(especially in case of non-fatal errors). Is it as soon as the error
happens, or are invocations batched at the end of the operation?
The reason I'm asking is because there is a long track of bugs related
to the global error handler [1], and it's likely that the
error_handler proposed here could suffer from the same issues. For
example the error_handler can reenter the operation being performed,
or change the state of the stream. For safety and simplicity, I would
suggest that error_handler calls are delayed until just before
returning to user code.
[1] https://github.com/php/php-src/issues/20018
Best Regards,
Arnaud
Hello,
I would like to introduce a new stream error handling RFC that is part of
my stream evolution work (PHP Foundation project funded by Sovereign Tech
Fund) :
Hi, I just published version 2.0 which is a significant redesign including:
-
Introducing StreamErrorStore, StreamErrorMode and StreamErrorCode enums
(the code also includes various suggested helper methods). The code is
backed (int) because I need to keep the number to easily match the category
and simplify the mapping. I think that in this case a backed enum is better. -
The error data are now in a StreamError class instead of an associative
array. This also simplifies StreamException which now has just a single
method to return the StreamError instance. The error handler callback
signature was also simplified to take just a single StreamError parameter
instead of multiple individual parameters. -
Errors are now grouped per operation and reported at the end of the call.
This also supports nested calls (e.g. in user streams) so they don't
overlap. And it delays the execution of error handlers (not just the stream
error callback but also the usual error handler if default standard error
triggering is used) which should be safer as noted by Arnaud. -
The storing of errors is done at the end of the operation grouping and
contains only the last grouped errors. So the function name was changed to
stream_get_last_error(). It returns just a single StreamError which has a
next property pointing to the next error if there is any. -
Stream is no longer exposed and errors are not collected on streams but
globally in the same way as wrappers. It is actually no longer even
collected per wrapper because it doesn't make much sense when the errors
are grouped per operation. -
Terminal (and NonTerminal) was renamed to Terminating (and
NonTerminating). -
Invalid enum types for context options (error_mode and error_store) now
throw TypeError. -
Implemented operation pooling to minimize memory allocations during error
handling (pre-allocated pool for up to 8 nested operations).
I hope this addresses all concerns.
Kind regards,
Jakub
Hi, I just published version 2.0 which is a significant redesign
including:
- Introducing ... StreamErrorCode
enums.... The
code is backed (int) because I need to keep the number to easily match
the category and simplify the mapping. I think that in this case a
backed enum is better.
Just spitballing here, but perhaps a StreamErrorCategory enum? The Code
could have a method/property that matches it to the corresponding
Category; categorising errors of different kinds can be done by
branching on the Category, rather than (possibly multiple) is*Error() tests.
Hi,
Hi, I just published version 2.0 which is a significant redesign
including:
- Introducing ... StreamErrorCode
enums.... The
code is backed (int) because I need to keep the number to easily match
the category and simplify the mapping. I think that in this case a
backed enum is better.Just spitballing here, but perhaps a StreamErrorCategory enum? The Code
could have a method/property that matches it to the corresponding
Category; categorising errors of different kinds can be done by
branching on the Category, rather than (possibly multiple) is*Error()
tests.
This is an interesting idea but it wouldn't work because some categories
are overlapping. So is*Error functions seem better fit here.
Kind regards,
Jakub
Am 29.12.2025 um 19:52 schrieb Jakub Zelenka bukka@php.net:
- The storing of errors is done at the end of the operation grouping and contains only the last grouped errors. So the function name was changed to stream_get_last_error(). It returns just a single StreamError which has a next property pointing to the next error if there is any.
I assume the chaining was based on Exception chaining but the example code under
https://wiki.php.net/rfc/stream_errors#error_chaining
looks a bit clunky to me and I was wondering if returning an array of StreamError would not be more straightforward, i.e. one could then use foreach, empty(), count(), array_first(), array_last(), array_find() etc. instead of methods like while, $e->count(), $e->hasCode() which would make it feel more idiomatic to me.
But then again it might be a matter of taste and/or I might have missed something.
Regards,
- Chris
Hi,
On Tue, Dec 30, 2025 at 9:24 AM Christian Schneider cschneid@cschneid.com
wrote:
Am 29.12.2025 um 19:52 schrieb Jakub Zelenka bukka@php.net:
- The storing of errors is done at the end of the operation grouping and
contains only the last grouped errors. So the function name was changed to
stream_get_last_error(). It returns just a single StreamError which has a
next property pointing to the next error if there is any.I assume the chaining was based on Exception chaining but the example code
under
https://wiki.php.net/rfc/stream_errors#error_chaining
looks a bit clunky to me and I was wondering if returning an array of
StreamError would not be more straightforward, i.e. one could then use
foreach, empty(),count(), array_first(), array_last(), array_find() etc.
instead of methods like while, $e->count(), $e->hasCode() which would make
it feel more idiomatic to me.
I thought about it and I think the exception like chaining makes a bit more
sense here. The thing is that in majority cases there will be only one
error so it would be most of the time array with just one element which
seems a bit wasteful. But if others also feel that array would be better, I
wouldn't mind to do follow up RFC as I don't really feel strongly about it.
Just let me know during the vote and if there's more people asking for it,
I will do a follow up RFC. Alternatively it could just implement iterator
or array access, which I was thinking about, but not sure if it's not too
messy.
Kind regards,
Jakub
Hi,
Hello,
I would like to introduce a new stream error handling RFC that is part of
my stream evolution work (PHP Foundation project funded by Sovereign Tech
Fund) :
As there has not been much discussion and keeping the patch up to date is a
slight pain, I plan to open voting on Friday (27/02/26) evening or Saturday
(28/02/26) morning unless some changes are required ofc.
Kind regards
Jakub
Hi Jakub,
I would like to introduce a new stream error handling RFC that is part of
my stream evolution work (PHP Foundation project funded by Sovereign Tech
Fund) :As there has not been much discussion and keeping the patch up to date is
a slight pain, I plan to open voting on Friday (27/02/26) evening or
Saturday (28/02/26) morning unless some changes are required ofc.
Thanks for the reminder! I discussed this with others and we raised the
following points:
- StreamErrorCode::None: do we need it?
Having an enum case representing "no error" feels a bit off to me. If an
API needs to express the absence of an error, would,'t StreamErrorCode|null
be more idiomatic? StreamErrorCode::None seems like a nullable value
disguised as an enum case, and it means callers always have to guard
against it, which somewhat defeats the purpose of using an enum. Am I
missing a use case where ::None is genuinely needed?
- StreamError::$next — is the naming intentional?
Since stream_get_last_error() returns the most recent error and the chain
travels backwards through time, $next seems to point to the previous error
chronologically. Would something like $previous (echoing
Throwable::getPrevious()) work better, or is the current naming deliberate?
- Should StreamErrorCode really be an enum?
The RFC lists in its "Future Scope" section: "Extension-specific error
ranges - Reserved ranges for extensions to define custom error codes."
This gave us pause. Enums in PHP are intentionally a closed, finite type:
their value is precisely that "invalid states become unrepresentable." If
extensions can define custom error codes at runtime, the set of possible
values would depend on which extensions are installed, and the type would
no longer be truly enumerable.
Larry touches on this exact tension in this post: when the value space
needs to be open or user-extensible, an enum is the wrong tool.
https://www.garfieldtech.com/blog/on-the-use-of-enums#open-type
I'd also expect the built-in list of codes to keep growing over time as
more wrappers and edge cases are covered; which is another hint the domain
may not be fixed.
Would a set of integer constants (possibly grouped in a class or interface)
be appropriate? It would be more honest about the open-ended nature of the
value space while still allowing meaningful comparisons, without creating
false expectations of exhaustiveness.
- Using stream_context_set_default to change error_mode looks hazardous
The RFC includes an example where stream_context_set_default is used to set
error_mode to StreamErrorMode::Exception globally. I'm worried about the
ecosystem impact here: if any library or application bootstrap does use
this, then existing packages using the common
@file_get_contents('maybe_existing_file') idiom could e.g. suddenly throw
uncaught exceptions, breaking behavior their authors had deliberately
chosen. This feels like a significant compatibility hazard for code that
doesn't control its full execution environment.
Would it be worth restricting error_mode (and possibly the other new
options) so that they can only be set via per-call contexts, not via
stream_context_set_default?
Thanks for working on this, we definitely need this!
Cheers,
Nicolas
Hi,
On Wed, Feb 25, 2026 at 7:11 PM Nicolas Grekas nicolas.grekas+php@gmail.com
wrote:
Hi Jakub,
I would like to introduce a new stream error handling RFC that is part of
my stream evolution work (PHP Foundation project funded by Sovereign Tech
Fund) :As there has not been much discussion and keeping the patch up to date is
a slight pain, I plan to open voting on Friday (27/02/26) evening or
Saturday (28/02/26) morning unless some changes are required ofc.Thanks for the reminder! I discussed this with others and we raised the
following points:
- StreamErrorCode::None: do we need it?
Having an enum case representing "no error" feels a bit off to me. If an
API needs to express the absence of an error, would,'t StreamErrorCode|null
be more idiomatic? StreamErrorCode::None seems like a nullable value
disguised as an enum case, and it means callers always have to guard
against it, which somewhat defeats the purpose of using an enum. Am I
missing a use case where ::None is genuinely needed?
Yeah this wouldn't make sense as enum but it would probs still make sense
to keep it if it changes to constant
- StreamError::$next — is the naming intentional?
Since stream_get_last_error() returns the most recent error and the chain
travels backwards through time, $next seems to point to the previous error
chronologically. Would something like $previous (echoing
Throwable::getPrevious()) work better, or is the current naming deliberate?
I will double check it but getPrevious() might make more sense.
- Should StreamErrorCode really be an enum?
The RFC lists in its "Future Scope" section: "Extension-specific error
ranges - Reserved ranges for extensions to define custom error codes."This gave us pause. Enums in PHP are intentionally a closed, finite type:
their value is precisely that "invalid states become unrepresentable." If
extensions can define custom error codes at runtime, the set of possible
values would depend on which extensions are installed, and the type would
no longer be truly enumerable.
Larry touches on this exact tension in this post: when the value space
needs to be open or user-extensible, an enum is the wrong tool.
https://www.garfieldtech.com/blog/on-the-use-of-enums#open-typeI'd also expect the built-in list of codes to keep growing over time as
more wrappers and edge cases are covered; which is another hint the domain
may not be fixed.Would a set of integer constants (possibly grouped in a class or
interface) be appropriate? It would be more honest about the open-ended
nature of the value space while still allowing meaningful comparisons,
without creating false expectations of exhaustiveness.
Ok if enum should be closed, then I agree that this should be changed
because there might be new errors. Larry suggested it before but I didn't
see any mention that enum should stay unchanged in future versions. I will
change it to StreamError class constants and add static helper
classification functions there.
- Using stream_context_set_default to change error_mode looks hazardous
The RFC includes an example where stream_context_set_default is used to
set error_mode to StreamErrorMode::Exception globally. I'm worried about
the ecosystem impact here: if any library or application bootstrap does use
this, then existing packages using the common
@file_get_contents('maybe_existing_file') idiom could e.g. suddenly throw
uncaught exceptions, breaking behavior their authors had deliberately
chosen. This feels like a significant compatibility hazard for code that
doesn't control its full execution environment.Would it be worth restricting error_mode (and possibly the other new
options) so that they can only be set via per-call contexts, not via
stream_context_set_default?
Hmm that's how stream context works and it would seem quite hacky to add
restriction on global context (we would basically need to add some
validation when global context is set). What's worse is that it would
prevent to change error handling for cases where it is not possible to set
context (fsockopen and various other cases). This is actually in some way
already possible by throwing from the stream notifications (limited to http
wrapper though) but I can see how it could have bigger impact here. I'm
afraid the libraries will have to deal with that which should eventually
lead to a better code.
Cheers,
Jakub
Le mer. 25 févr. 2026 à 20:16, Jakub Zelenka bukka@php.net a écrit :
Hi,
On Wed, Feb 25, 2026 at 7:11 PM Nicolas Grekas <
nicolas.grekas+php@gmail.com> wrote:Hi Jakub,
I would like to introduce a new stream error handling RFC that is part of
my stream evolution work (PHP Foundation project funded by Sovereign Tech
Fund) :As there has not been much discussion and keeping the patch up to date
is a slight pain, I plan to open voting on Friday (27/02/26) evening or
Saturday (28/02/26) morning unless some changes are required ofc.Thanks for the reminder! I discussed this with others and we raised the
following points:
- StreamErrorCode::None: do we need it?
Having an enum case representing "no error" feels a bit off to me. If an
API needs to express the absence of an error, would,'t StreamErrorCode|null
be more idiomatic? StreamErrorCode::None seems like a nullable value
disguised as an enum case, and it means callers always have to guard
against it, which somewhat defeats the purpose of using an enum. Am I
missing a use case where ::None is genuinely needed?Yeah this wouldn't make sense as enum but it would probs still make sense
to keep it if it changes to constant
- StreamError::$next — is the naming intentional?
Since stream_get_last_error() returns the most recent error and the chain
travels backwards through time, $next seems to point to the previous error
chronologically. Would something like $previous (echoing
Throwable::getPrevious()) work better, or is the current naming deliberate?I will double check it but getPrevious() might make more sense.
- Should StreamErrorCode really be an enum?
The RFC lists in its "Future Scope" section: "Extension-specific error
ranges - Reserved ranges for extensions to define custom error codes."This gave us pause. Enums in PHP are intentionally a closed, finite type:
their value is precisely that "invalid states become unrepresentable." If
extensions can define custom error codes at runtime, the set of possible
values would depend on which extensions are installed, and the type would
no longer be truly enumerable.
Larry touches on this exact tension in this post: when the value space
needs to be open or user-extensible, an enum is the wrong tool.
https://www.garfieldtech.com/blog/on-the-use-of-enums#open-typeI'd also expect the built-in list of codes to keep growing over time as
more wrappers and edge cases are covered; which is another hint the domain
may not be fixed.Would a set of integer constants (possibly grouped in a class or
interface) be appropriate? It would be more honest about the open-ended
nature of the value space while still allowing meaningful comparisons,
without creating false expectations of exhaustiveness.Ok if enum should be closed, then I agree that this should be changed
because there might be new errors. Larry suggested it before but I didn't
see any mention that enum should stay unchanged in future versions. I will
change it to StreamError class constants and add static helper
classification functions there.
- Using stream_context_set_default to change error_mode looks hazardous
The RFC includes an example where stream_context_set_default is used to
set error_mode to StreamErrorMode::Exception globally. I'm worried about
the ecosystem impact here: if any library or application bootstrap does use
this, then existing packages using the common
@file_get_contents('maybe_existing_file') idiom could e.g. suddenly throw
uncaught exceptions, breaking behavior their authors had deliberately
chosen. This feels like a significant compatibility hazard for code that
doesn't control its full execution environment.Would it be worth restricting error_mode (and possibly the other new
options) so that they can only be set via per-call contexts, not via
stream_context_set_default?Hmm that's how stream context works and it would seem quite hacky to add
restriction on global context (we would basically need to add some
validation when global context is set). What's worse is that it would
prevent to change error handling for cases where it is not possible to set
context (fsockopen and various other cases). This is actually in some way
already possible by throwing from the stream notifications (limited to http
wrapper though) but I can see how it could have bigger impact here. I'm
afraid the libraries will have to deal with that which should eventually
lead to a better code.
Thanks for acknowledging the first items, I'm looking forward to the
update.
About this last one, I need to insist: that global behavior is going to be
a nightmare. Everytime someone proposes to add a new ini setting to
configure some global behavior, we say so. The reasons are exactly the
same. Existing code will just have to be rewritten, which might lead to
better code but also to high friction. Exactly like a BC break - it'll be
one actually.
About adding validation to stream_context_set_default, that looks like a
non issue to me.
About fsockopen et al, that's a very good reason to add a context argument.
Without that, properly using those function would mean changing the global
stat all the time. Better not plan for this.
Cheers,
Nicolas
Hi,
On Thu, Feb 26, 2026 at 7:56 AM Nicolas Grekas nicolas.grekas+php@gmail.com
wrote:
Le mer. 25 févr. 2026 à 20:16, Jakub Zelenka bukka@php.net a écrit :
Hi,
On Wed, Feb 25, 2026 at 7:11 PM Nicolas Grekas <
nicolas.grekas+php@gmail.com> wrote:Hi Jakub,
I would like to introduce a new stream error handling RFC that is part
of my stream evolution work (PHP Foundation project funded by Sovereign
Tech Fund) :As there has not been much discussion and keeping the patch up to date
is a slight pain, I plan to open voting on Friday (27/02/26) evening or
Saturday (28/02/26) morning unless some changes are required ofc.Thanks for the reminder! I discussed this with others and we raised the
following points:
- StreamErrorCode::None: do we need it?
Having an enum case representing "no error" feels a bit off to me. If an
API needs to express the absence of an error, would,'t StreamErrorCode|null
be more idiomatic? StreamErrorCode::None seems like a nullable value
disguised as an enum case, and it means callers always have to guard
against it, which somewhat defeats the purpose of using an enum. Am I
missing a use case where ::None is genuinely needed?Yeah this wouldn't make sense as enum but it would probs still make sense
to keep it if it changes to constant
- StreamError::$next — is the naming intentional?
Since stream_get_last_error() returns the most recent error and the
chain travels backwards through time, $next seems to point to the previous
error chronologically. Would something like $previous (echoing
Throwable::getPrevious()) work better, or is the current naming deliberate?I will double check it but getPrevious() might make more sense.
- Should StreamErrorCode really be an enum?
The RFC lists in its "Future Scope" section: "Extension-specific error
ranges - Reserved ranges for extensions to define custom error codes."This gave us pause. Enums in PHP are intentionally a closed, finite
type: their value is precisely that "invalid states become
unrepresentable." If extensions can define custom error codes at runtime,
the set of possible values would depend on which extensions are installed,
and the type would no longer be truly enumerable.
Larry touches on this exact tension in this post: when the value space
needs to be open or user-extensible, an enum is the wrong tool.
https://www.garfieldtech.com/blog/on-the-use-of-enums#open-typeI'd also expect the built-in list of codes to keep growing over time as
more wrappers and edge cases are covered; which is another hint the domain
may not be fixed.Would a set of integer constants (possibly grouped in a class or
interface) be appropriate? It would be more honest about the open-ended
nature of the value space while still allowing meaningful comparisons,
without creating false expectations of exhaustiveness.Ok if enum should be closed, then I agree that this should be changed
because there might be new errors. Larry suggested it before but I didn't
see any mention that enum should stay unchanged in future versions. I will
change it to StreamError class constants and add static helper
classification functions there.
- Using stream_context_set_default to change error_mode looks hazardous
The RFC includes an example where stream_context_set_default is used to
set error_mode to StreamErrorMode::Exception globally. I'm worried about
the ecosystem impact here: if any library or application bootstrap does use
this, then existing packages using the common
@file_get_contents('maybe_existing_file') idiom could e.g. suddenly throw
uncaught exceptions, breaking behavior their authors had deliberately
chosen. This feels like a significant compatibility hazard for code that
doesn't control its full execution environment.Would it be worth restricting error_mode (and possibly the other new
options) so that they can only be set via per-call contexts, not via
stream_context_set_default?Hmm that's how stream context works and it would seem quite hacky to add
restriction on global context (we would basically need to add some
validation when global context is set). What's worse is that it would
prevent to change error handling for cases where it is not possible to set
context (fsockopen and various other cases). This is actually in some way
already possible by throwing from the stream notifications (limited to http
wrapper though) but I can see how it could have bigger impact here. I'm
afraid the libraries will have to deal with that which should eventually
lead to a better code.Thanks for acknowledging the first items, I'm looking forward to the
update.About this last one, I need to insist: that global behavior is going to be
a nightmare. Everytime someone proposes to add a new ini setting to
configure some global behavior, we say so. The reasons are exactly the
same. Existing code will just have to be rewritten, which might lead to
better code but also to high friction. Exactly like a BC break - it'll be
one actually.About adding validation to stream_context_set_default, that looks like a
non issue to me.
About fsockopen et al, that's a very good reason to add a context
argument. Without that, properly using those function would mean changing
the global stat all the time. Better not plan for this.
Ok I think you are right that the BC impact would be just too big so I will
add that validation.
Kind regards,
Jakub
Hi,
On Wed, Feb 25, 2026 at 7:11 PM Nicolas Grekas nicolas.grekas+php@gmail.com
wrote:
Hi Jakub,
I would like to introduce a new stream error handling RFC that is part of
my stream evolution work (PHP Foundation project funded by Sovereign Tech
Fund) :
I just updated implementation and RFC to version 2.1 which addresses the
below issues.
As there has not been much discussion and keeping the patch up to date is
a slight pain, I plan to open voting on Friday (27/02/26) evening or
Saturday (28/02/26) morning unless some changes are required ofc.
The update means that the vote will not happen in the next two weeks...
Thanks for the reminder! I discussed this with others and we raised the
following points:
- StreamErrorCode::None: do we need it?
Having an enum case representing "no error" feels a bit off to me. If an
API needs to express the absence of an error, would,'t StreamErrorCode|null
be more idiomatic? StreamErrorCode::None seems like a nullable value
disguised as an enum case, and it means callers always have to guard
against it, which somewhat defeats the purpose of using an enum. Am I
missing a use case where ::None is genuinely needed?
As I removed the enum this is no longer issue. I kept none as constant for
comparing as it might be useful.
- StreamError::$next — is the naming intentional?
Since stream_get_last_error() returns the most recent error and the chain
travels backwards through time, $next seems to point to the previous error
chronologically. Would something like $previous (echoing
Throwable::getPrevious()) work better, or is the current naming deliberate?
I checked this one and realised that $next is actually better because it's
better to keep the first error which for streams is really the useful one.
The follow up errors (if any - most of the time there's just one) are most
of the time not that useful but might add a bit more context so that's why
they are chained. I added this reasoning to the RFC.
- Should StreamErrorCode really be an enum?
The RFC lists in its "Future Scope" section: "Extension-specific error
ranges - Reserved ranges for extensions to define custom error codes."This gave us pause. Enums in PHP are intentionally a closed, finite type:
their value is precisely that "invalid states become unrepresentable." If
extensions can define custom error codes at runtime, the set of possible
values would depend on which extensions are installed, and the type would
no longer be truly enumerable.
Larry touches on this exact tension in this post: when the value space
needs to be open or user-extensible, an enum is the wrong tool.
https://www.garfieldtech.com/blog/on-the-use-of-enums#open-typeI'd also expect the built-in list of codes to keep growing over time as
more wrappers and edge cases are covered; which is another hint the domain
may not be fixed.Would a set of integer constants (possibly grouped in a class or
interface) be appropriate? It would be more honest about the open-ended
nature of the value space while still allowing meaningful comparisons,
without creating false expectations of exhaustiveness.
I changed it to the StreamError class constants and also move the is*Error
functions there.
- Using stream_context_set_default to change error_mode looks hazardous
The RFC includes an example where stream_context_set_default is used to
set error_mode to StreamErrorMode::Exception globally. I'm worried about
the ecosystem impact here: if any library or application bootstrap does use
this, then existing packages using the common
@file_get_contents('maybe_existing_file') idiom could e.g. suddenly throw
uncaught exceptions, breaking behavior their authors had deliberately
chosen. This feels like a significant compatibility hazard for code that
doesn't control its full execution environment.Would it be worth restricting error_mode (and possibly the other new
options) so that they can only be set via per-call contexts, not via
stream_context_set_default?
I added that restriction and also added context to some stream functions so
it can be set explicitly. There will be more function extended in the
future if this passes.
Hope it's ok now! If there's anything else, please let me know.
Cheers
Jakub
Le ven. 27 févr. 2026, 22:21, Jakub Zelenka bukka@php.net a écrit :
Hi,
On Wed, Feb 25, 2026 at 7:11 PM Nicolas Grekas <
nicolas.grekas+php@gmail.com> wrote:Hi Jakub,
I would like to introduce a new stream error handling RFC that is part of
my stream evolution work (PHP Foundation project funded by Sovereign Tech
Fund) :I just updated implementation and RFC to version 2.1 which addresses the
below issues.As there has not been much discussion and keeping the patch up to date is
a slight pain, I plan to open voting on Friday (27/02/26) evening or
Saturday (28/02/26) morning unless some changes are required ofc.The update means that the vote will not happen in the next two weeks...
Thanks for the reminder! I discussed this with others and we raised the
following points:
- StreamErrorCode::None: do we need it?
Having an enum case representing "no error" feels a bit off to me. If an
API needs to express the absence of an error, would,'t StreamErrorCode|null
be more idiomatic? StreamErrorCode::None seems like a nullable value
disguised as an enum case, and it means callers always have to guard
against it, which somewhat defeats the purpose of using an enum. Am I
missing a use case where ::None is genuinely needed?As I removed the enum this is no longer issue. I kept none as constant for
comparing as it might be useful.
- StreamError::$next — is the naming intentional?
Since stream_get_last_error() returns the most recent error and the chain
travels backwards through time, $next seems to point to the previous error
chronologically. Would something like $previous (echoing
Throwable::getPrevious()) work better, or is the current naming deliberate?I checked this one and realised that $next is actually better because it's
better to keep the first error which for streams is really the useful one.
The follow up errors (if any - most of the time there's just one) are most
of the time not that useful but might add a bit more context so that's why
they are chained. I added this reasoning to the RFC.
- Should StreamErrorCode really be an enum?
The RFC lists in its "Future Scope" section: "Extension-specific error
ranges - Reserved ranges for extensions to define custom error codes."This gave us pause. Enums in PHP are intentionally a closed, finite type:
their value is precisely that "invalid states become unrepresentable." If
extensions can define custom error codes at runtime, the set of possible
values would depend on which extensions are installed, and the type would
no longer be truly enumerable.
Larry touches on this exact tension in this post: when the value space
needs to be open or user-extensible, an enum is the wrong tool.
https://www.garfieldtech.com/blog/on-the-use-of-enums#open-typeI'd also expect the built-in list of codes to keep growing over time as
more wrappers and edge cases are covered; which is another hint the domain
may not be fixed.Would a set of integer constants (possibly grouped in a class or
interface) be appropriate? It would be more honest about the open-ended
nature of the value space while still allowing meaningful comparisons,
without creating false expectations of exhaustiveness.I changed it to the StreamError class constants and also move the is*Error
functions there.
- Using stream_context_set_default to change error_mode looks hazardous
The RFC includes an example where stream_context_set_default is used to
set error_mode to StreamErrorMode::Exception globally. I'm worried about
the ecosystem impact here: if any library or application bootstrap does use
this, then existing packages using the common
@file_get_contents('maybe_existing_file') idiom could e.g. suddenly throw
uncaught exceptions, breaking behavior their authors had deliberately
chosen. This feels like a significant compatibility hazard for code that
doesn't control its full execution environment.Would it be worth restricting error_mode (and possibly the other new
options) so that they can only be set via per-call contexts, not via
stream_context_set_default?I added that restriction and also added context to some stream functions
so it can be set explicitly. There will be more function extended in the
future if this passes.Hope it's ok now! If there's anything else, please let me know.
Looks nice thanks!
I'd just explicitly tell what happens when one tries to change the error
mode globally. An exception? Which one?
Cheers,
Nicolas
On Fri, Feb 27, 2026 at 11:34 PM Nicolas Grekas <
nicolas.grekas+php@gmail.com> wrote:
Le ven. 27 févr. 2026, 22:21, Jakub Zelenka bukka@php.net a écrit :
Hi,
On Wed, Feb 25, 2026 at 7:11 PM Nicolas Grekas <
nicolas.grekas+php@gmail.com> wrote:Hi Jakub,
I would like to introduce a new stream error handling RFC that is part
of my stream evolution work (PHP Foundation project funded by Sovereign
Tech Fund) :I just updated implementation and RFC to version 2.1 which addresses the
below issues.As there has not been much discussion and keeping the patch up to date
is a slight pain, I plan to open voting on Friday (27/02/26) evening or
Saturday (28/02/26) morning unless some changes are required ofc.The update means that the vote will not happen in the next two weeks...
Thanks for the reminder! I discussed this with others and we raised the
following points:
- StreamErrorCode::None: do we need it?
Having an enum case representing "no error" feels a bit off to me. If an
API needs to express the absence of an error, would,'t StreamErrorCode|null
be more idiomatic? StreamErrorCode::None seems like a nullable value
disguised as an enum case, and it means callers always have to guard
against it, which somewhat defeats the purpose of using an enum. Am I
missing a use case where ::None is genuinely needed?As I removed the enum this is no longer issue. I kept none as constant
for comparing as it might be useful.
- StreamError::$next — is the naming intentional?
Since stream_get_last_error() returns the most recent error and the
chain travels backwards through time, $next seems to point to the previous
error chronologically. Would something like $previous (echoing
Throwable::getPrevious()) work better, or is the current naming deliberate?I checked this one and realised that $next is actually better because
it's better to keep the first error which for streams is really the useful
one. The follow up errors (if any - most of the time there's just one) are
most of the time not that useful but might add a bit more context so that's
why they are chained. I added this reasoning to the RFC.
- Should StreamErrorCode really be an enum?
The RFC lists in its "Future Scope" section: "Extension-specific error
ranges - Reserved ranges for extensions to define custom error codes."This gave us pause. Enums in PHP are intentionally a closed, finite
type: their value is precisely that "invalid states become
unrepresentable." If extensions can define custom error codes at runtime,
the set of possible values would depend on which extensions are installed,
and the type would no longer be truly enumerable.
Larry touches on this exact tension in this post: when the value space
needs to be open or user-extensible, an enum is the wrong tool.
https://www.garfieldtech.com/blog/on-the-use-of-enums#open-typeI'd also expect the built-in list of codes to keep growing over time as
more wrappers and edge cases are covered; which is another hint the domain
may not be fixed.Would a set of integer constants (possibly grouped in a class or
interface) be appropriate? It would be more honest about the open-ended
nature of the value space while still allowing meaningful comparisons,
without creating false expectations of exhaustiveness.I changed it to the StreamError class constants and also move the
is*Error functions there.
- Using stream_context_set_default to change error_mode looks hazardous
The RFC includes an example where stream_context_set_default is used to
set error_mode to StreamErrorMode::Exception globally. I'm worried about
the ecosystem impact here: if any library or application bootstrap does use
this, then existing packages using the common
@file_get_contents('maybe_existing_file') idiom could e.g. suddenly throw
uncaught exceptions, breaking behavior their authors had deliberately
chosen. This feels like a significant compatibility hazard for code that
doesn't control its full execution environment.Would it be worth restricting error_mode (and possibly the other new
options) so that they can only be set via per-call contexts, not via
stream_context_set_default?I added that restriction and also added context to some stream functions
so it can be set explicitly. There will be more function extended in the
future if this passes.Hope it's ok now! If there's anything else, please let me know.
Looks nice thanks!
I'd just explicitly tell what happens when one tries to change the error
mode globally. An exception? Which one?
Ok updated it - it's ValueError...
Cheers
Jakub
Hi
thank you for your RFC. I'm regretfully late to the party, but my
primary comment about the RFC relates to the latest changes.
Specifically:
Am 2026-02-27 23:21, schrieb Jakub Zelenka:
I changed it to the StreamError class constants and also move the
is*Error
functions there.
The RFC specifically defines “ranges” for the numeric value of the error
constants, some of which are quite small with only 10 values available.
I believe this is dangerous, because it is possible to run out of values
within a specific range and users might make assumptions based on the
ranges when they really should rely on the is*() methods to check what
type of error there is. This is not a problem that would exist if the
error code was an unbacked enum.
Contrary to Nicolas, I also don't think it is a problem for the error
code enum to be extended in future PHP versions. In other programming
languages, Rust specifically, it is also common for errors to be defined
as an enum that is explicitly defined to not be exhaustive. Here's an
example in the standard library:
https://doc.rust-lang.org/std/io/enum.ErrorKind.html. Note how the
documentation says:
In application code, use match for the ErrorKind values you are
expecting; use _ to match “all other errors”.
And the same would be possible in PHP by using a default branch in a
match() expression. We could also add a “NonExhaustiveEnum” interface
(or attribute) to PHP as a helper indicator for static analysis tools to
warn if a default match is missing.
As for the StreamError class being a linked list: I agree with the
folks that mentioned that the returned errors should be an array
instead. count() would naturally be available, hasCode() can be
implemented with array_any($errors, fn ($error) => $error->code === CODE_DECODING_FAILED) and the “primary error” can be obtained with
array_first() (which we added in PHP 8.5).
For the naming of stream_get_last_error(): Within PHP we have both
X_get_last_error() and X_last_error(). The latter seems to be more
common and also what I would prefer here, because the stream_get_
prefix sounds to me like we would get something from a stream, but the
returned value is not related to a specific stream, but rather a global.
Best regards
Tim Düsterhus
Hi,
Hi
thank you for your RFC. I'm regretfully late to the party, but my
primary comment about the RFC relates to the latest changes.
Specifically:Am 2026-02-27 23:21, schrieb Jakub Zelenka:
I changed it to the StreamError class constants and also move the
is*Error
functions there.The RFC specifically defines “ranges” for the numeric value of the error
constants, some of which are quite small with only 10 values available.
I believe this is dangerous, because it is possible to run out of values
within a specific range and users might make assumptions based on the
ranges when they really should rely on theis*()methods to check what
type of error there is. This is not a problem that would exist if the
error code was an unbacked enum.Contrary to Nicolas, I also don't think it is a problem for the error
code enum to be extended in future PHP versions. In other programming
languages, Rust specifically, it is also common for errors to be defined
as an enum that is explicitly defined to not be exhaustive. Here's an
example in the standard library:
https://doc.rust-lang.org/std/io/enum.ErrorKind.html. Note how the
documentation says:
I agree with this and as you pointed out privately we already have
precedent in URI extension - enum UrlValidationErrorType:
https://github.com/php/php-src/blob/5e45c17d817df003cd24109f0ae222c5d82fecd1/ext/uri/php_uri.stub.php#L112-L143
.
The constant were also not user friendly as they were just numbers so we
would need some look up function to produce name. Especially painful for
logging which is probably the main use case here.
I changed it back to enum and this time I made it non backed which was
possible to do in a nicer way due to recent changes that Arnaud did for
declaration headers containing C enums. It makes the whole implementation
much nicer actually. I just extended the gen_stub.php to optionally produce
look up table for enum name which should speed up and simplify the look
ups.
As for the StreamError class being a linked list: I agree with the
folks that mentioned that the returned errors should be an array
instead.count()would naturally be available,hasCode()can be
implemented witharray_any($errors, fn ($error) => $error->code === CODE_DECODING_FAILED)and the “primary error” can be obtained with
array_first()(which we added in PHP 8.5).
As you were the second person to ask for it and that linked list was not
really a PHP thing, I changed it as suggested. This changed API in more
places to accommodate for it but think for logging and other use cases, it
will be a bit nicer at the end.
For the naming of
stream_get_last_error(): Within PHP we have both
X_get_last_error()andX_last_error(). The latter seems to be more
common and also what I would prefer here, because thestream_get_
prefix sounds to me like we would get something from a stream, but the
returned value is not related to a specific stream, but rather a global.
Good point, I changed it but because it now returns array (no linked list),
it's called stream_last_errors(). I also added stream_clear_errors for
explicit clearing which might be useful in some situations.
The RFC and the implementation is updated so please take a look!
Kind regards,
Jakub
Hi
Am 2026-03-29 20:25, schrieb Jakub Zelenka:
For the naming of
stream_get_last_error(): Within PHP we have both
X_get_last_error()andX_last_error(). The latter seems to be more
common and also what I would prefer here, because thestream_get_
prefix sounds to me like we would get something from a stream, but the
returned value is not related to a specific stream, but rather a
global.Good point, I changed it but because it now returns array (no linked
list),
it's called stream_last_errors(). I also added stream_clear_errors for
explicit clearing which might be useful in some situations.
That both makes sense to me.
The RFC and the implementation is updated so please take a look!
Thank you. The updated RFC looks really good now. I have some (final?)
minor remarks:
- "// Search for specific codes using array_any (PHP 8.5+)"
array_any is already available in PHP 8.4. The same is true for
"array_find (PHP 8.5+)" in the same example. But given it's an RFC for
PHP 8.6 anyways, we already know that these functions exist, so the hint
could just be removed entirely.
- In the example example "$primary = $errors[0] ?? null;"
This can just be $primary = array_first($errors); (PHP 8.5+). Same for
the other examples. The examples should ideally show the cleanest
possible code :-)
- For "StreamErrorCode::is*()"
Can error codes fall into multiple categories or is it always a single
one? If it's guaranteed to be a single category, then perhaps a
->getErrorCategory() method returning a StreamErrorCodeCategory enum
makes more sense and allow for simpler / more efficient code when folks
are interested in checking for multiple different categories. Instead of
$code->isNetworkError() || $code->isFileSystemError() they can do
\in_array($code->getErrorCategory(), [StreamErrorCodeCategory::NetworkError, StreamErrorCodeCategory::FileSystemError], true) or use a match()
expression instead.
Best regards
Tim Düsterhus
Hi Tim, Jakub,
Hi
Am 2026-03-29 20:25, schrieb Jakub Zelenka:
For the naming of
stream_get_last_error(): Within PHP we have both
X_get_last_error()andX_last_error(). The latter seems to be more
common and also what I would prefer here, because thestream_get_
prefix sounds to me like we would get something from a stream, but the
returned value is not related to a specific stream, but rather a
global.Good point, I changed it but because it now returns array (no linked
list),
it's called stream_last_errors(). I also added stream_clear_errors for
explicit clearing which might be useful in some situations.That both makes sense to me.
The RFC and the implementation is updated so please take a look!
...
- For "StreamErrorCode::is*()"
Can error codes fall into multiple categories or is it always a single
one? If it's guaranteed to be a single category, then perhaps a
->getErrorCategory()method returning a StreamErrorCodeCategory enum
makes more sense and allow for simpler / more efficient code when folks
are interested in checking for multiple different categories. Instead of
$code->isNetworkError() || $code->isFileSystemError()they can do
\in_array($code->getErrorCategory(), [StreamErrorCodeCategory::NetworkError, StreamErrorCodeCategory::FileSystemError], true)or use amatch()
These points actually are on the spot about what I've been trying to
raise earlier in this thread, or they demonstrate it nicely.
Suggesting a getErrorCategory() method or a StreamErrorCodeCategory
enum to make category checks cleaner. That's a reasonable API
improvement, in the context of what the RFC proposes in the current
state. But it also highlights the underlying design issue: we're
building a parallel categorization system on top of error codes, when
the type system already provides exactly this, through exception
subclasses, for free. That reminds a bit of the early OO's time in php
when all we did was to wrap the legacy procedural implementation in
dumb OO wrapper, saving, if lucky, user's keystrokes. That's not even
the case here.
The point is that a stream operation can produce errors of different
categories and therefore the exception "cannot be reasonably
categorised": this is how exceptions have always worked. One throws
the primary exception, the one that caused the operation to fail, and
chain additional context via $previous or attach it as metadata. The
caller catches what they care about:
catch (StreamNetworkException $e) {...}
The exception type answers "what do I do about this?" The error chain
answers exactly what happens, where in detail. These are different
questions for different uses, and conflating them is what leads to the
current design where it catches a flat StreamException and then has to
inspect its contents to find out what kind of failure it was.
getErrorCategory() suggestion would give us
match($e->getErrors()[0]->getErrorCategory()) { ... } inside a catch
block. That's essentially reimplementing what catch
(StreamNetworkException $e) already does, except the type system can't
help, static analysis can't reason about it, and it can't compose it
with other exception handling in a codebase.
The current RFC introduces exception syntax without exception
semantics. The isFileSystemError(), isNetworkError() methods, and now
potentially getErrorCategory(). are workarounds for the absence of a
typed hierarchy, not features we should need in the 1st place.
Since StreamException does not exist in any current PHP version, there
is zero BC cost to making it a proper base class now. It takes more
effort to design such additions correctly, however it would be a
significant improvement in the long run for php's stream, beyond a
current internal refactoring and long due cleaning :).
--
Pierre
Hi,
Hi Tim, Jakub,
Hi
Am 2026-03-29 20:25, schrieb Jakub Zelenka:
For the naming of
stream_get_last_error(): Within PHP we have both
X_get_last_error()andX_last_error(). The latter seems to be more
common and also what I would prefer here, because thestream_get_
prefix sounds to me like we would get something from a stream, but the
returned value is not related to a specific stream, but rather a
global.Good point, I changed it but because it now returns array (no linked
list),
it's called stream_last_errors(). I also added stream_clear_errors for
explicit clearing which might be useful in some situations.That both makes sense to me.
The RFC and the implementation is updated so please take a look!
...
- For "StreamErrorCode::is*()"
Can error codes fall into multiple categories or is it always a single
one? If it's guaranteed to be a single category, then perhaps a
->getErrorCategory()method returning a StreamErrorCodeCategory enum
makes more sense and allow for simpler / more efficient code when folks
are interested in checking for multiple different categories. Instead of
$code->isNetworkError() || $code->isFileSystemError()they can do
\in_array($code->getErrorCategory(), [StreamErrorCodeCategory::NetworkError, StreamErrorCodeCategory::FileSystemError], true)or use amatch()These points actually are on the spot about what I've been trying to
raise earlier in this thread, or they demonstrate it nicely.Suggesting a getErrorCategory() method or a StreamErrorCodeCategory
enum to make category checks cleaner. That's a reasonable API
improvement, in the context of what the RFC proposes in the current
state. But it also highlights the underlying design issue: we're
building a parallel categorization system on top of error codes, when
the type system already provides exactly this, through exception
subclasses, for free. That reminds a bit of the early OO's time in php
when all we did was to wrap the legacy procedural implementation in
dumb OO wrapper, saving, if lucky, user's keystrokes. That's not even
the case here.The point is that a stream operation can produce errors of different
categories and therefore the exception "cannot be reasonably
categorised": this is how exceptions have always worked. One throws
the primary exception, the one that caused the operation to fail, and
chain additional context via $previous or attach it as metadata. The
caller catches what they care about:catch (StreamNetworkException $e) {...}
The exception type answers "what do I do about this?" The error chain
answers exactly what happens, where in detail. These are different
questions for different uses, and conflating them is what leads to the
current design where it catches a flat StreamException and then has to
inspect its contents to find out what kind of failure it was.getErrorCategory() suggestion would give us
match($e->getErrors()[0]->getErrorCategory()) { ... } inside a catch
block. That's essentially reimplementing what catch
(StreamNetworkException $e) already does, except the type system can't
help, static analysis can't reason about it, and it can't compose it
with other exception handling in a codebase.The current RFC introduces exception syntax without exception
semantics. The isFileSystemError(), isNetworkError() methods, and now
potentially getErrorCategory(). are workarounds for the absence of a
typed hierarchy, not features we should need in the 1st place.
Thanks for the thorough feedback. After some considering I decided not to
expose those helpers at this stage. The reason is that the current
classification is a bit uncertain and I want to have more final set of
error codes before anything like this is introduced. The current setup was
a bit random and sometimes it could go to multiple categories so there are
some decisions to be made. Also the is* methods were not the best design as
pointed out and other option should be considered. I'm also not too sure
about usefulness of categories. All of this should be decided on its own
and later.
Since StreamException does not exist in any current PHP version, there
is zero BC cost to making it a proper base class now. It takes more
effort to design such additions correctly, however it would be a
significant improvement in the long run for php's stream, beyond a
current internal refactoring and long due cleaning :).
I don't see how it takes more effort later. There is also pretty much no BC
break even if we went if subclassing exceptions as I noted before. I think
actually exactly opposite here. If we rush it and use incorrect categories
for some errors, it will be a BC break to correct so this needs a proper
considering and this feature should be added later.
Kind regards,
Jakub
Hi
- "// Search for specific codes using array_any (PHP 8.5+)"
array_any is already available in PHP 8.4. The same is true for
"array_find (PHP 8.5+)" in the same example. But given it's an RFC for
PHP 8.6 anyways, we already know that these functions exist, so the hint
could just be removed entirely.
Fixed
- In the example example "$primary = $errors[0] ?? null;"
This can just be
$primary = array_first($errors);(PHP 8.5+). Same for
the other examples. The examples should ideally show the cleanest
possible code :-)
Fixed
- For "StreamErrorCode::is*()"
Can error codes fall into multiple categories or is it always a single
one? If it's guaranteed to be a single category, then perhaps a
->getErrorCategory()method returning a StreamErrorCodeCategory enum
makes more sense and allow for simpler / more efficient code when folks
are interested in checking for multiple different categories. Instead of
$code->isNetworkError() || $code->isFileSystemError()they can do
\in_array($code->getErrorCategory(), [StreamErrorCodeCategory::NetworkError, StreamErrorCodeCategory::FileSystemError], true)or use amatch()
expression instead.
After doing a review of the categories, I decided to drop all the helpers
and not expose categories in any way. More on the reasoning in my other
replay to Pierre.
Kind regards,
Jakub
Hi
Am 2026-04-02 19:25, schrieb Jakub Zelenka:
After doing a review of the categories, I decided to drop all the
helpers
and not expose categories in any way. More on the reasoning in my other
replay to Pierre.
That works for me, better start with only the things we are sure about
and then add the other bits in a future PHP version.
I've now given the RFC another full read and I don't have further
comments. It's looking good to me now and I'm really looking forward to
this improved error handling for streams!
Best regards
Tim Düsterhus
hi,
Hi
Am 2026-04-02 19:25, schrieb Jakub Zelenka:
After doing a review of the categories, I decided to drop all the
helpers
and not expose categories in any way. More on the reasoning in my other
replay to Pierre.That works for me, better start with only the things we are sure about
and then add the other bits in a future PHP version.I've now given the RFC another full read and I don't have further
comments. It's looking good to me now and I'm really looking forward to
this improved error handling for streams!
same here, if or when it goes, it would be good to have two votes, the
cleaning and refactoring, and the exception addition, to which i will vote
against as explained in my earlier replies.
best,
Pierre
@pierrejoye | http://www.libgd.org
Hi Jakub,
Hello,
I would like to introduce a new stream error handling RFC that is part of my stream evolution work (PHP Foundation project funded by Sovereign Tech Fund) :
Thank you for this RFC! . It is clearly needed and the error chaining
design is great. There are a few points I'd like to raise, the last
two being slightly off topic specifically to that rfc but however very
related to. So a slightly long reply.
- New exceptions
The current design has a single flat StreamException. This means the
exception path requires inspecting the error object to branch on
category:
catch (StreamException $e) {
if ($e->getError()->isNetworkError()) {
// retry
} elseif ($e->getError()->code === StreamError::CODE_NOT_FOUND) {
// fallback
}
}
The current design introduces StreamException, which is good. But as a
single flat class it doesn't actually allow to use exceptions the way
exceptions are meant to be used. One still has to inspect the error
object inside the catch block to know what happened. It introduces the
syntax of exception handling without the benefit of it.
Since the error categories are already well-defined and stable in this
RFC, StreamException could be a base class with typed subclasses
mirroring those categories:
StreamException
├ StreamIoException
├ StreamFileSystemException
├ StreamNetworkException
├ StreamWrapperException
└ ...
This allows the more natural and idiomatic:
catch (StreamNetworkException $e) {
// retry — type alone tells you what happened
} catch (StreamNotFoundException $e) {
// fallback
} catch (StreamException $e) {
// generic
}
The StreamError value object can stay as in the RFC (and keep "BC" for
the error const's bag), it's appropriate for the inspection/silent
mode use case. I see the typed exception hierarchy and the flat value
object serve different purposes and complement each other:
$e->getError() still gives you full detail when you need it. These are
separable concerns currently merged into one class.
Since StreamException doesn't exist in any current PHP version, there
is no existing userland code catching it. This is a clean state.
Adding a typed hierarchy now costs nothing in BC terms, and
retrofitting it later would be a BC break.
It is more than just syntactic sugar, it allows very clean logic.
static analysis tools can reason about which exceptions a function
throws, IDEs can suggest catch blocks, and retry wrappers can target
network errors specifically without accidentally swallowing filesystem
errors. The cost of adding the hierarchy now is low; retrofitting it
later is a BC break.
- Custom stream wrapper and errors
How does custom wrappers, be in ext/, or out of core wrappers, exts or
user land, can emit errors? Or is it out of the scope of this RFC?
For wrappers authors, f.e. in zip, php_error_docref(NULL, E_WARNING,
"Zip stream error: %s", zip_error_strerror(err));, or
php_stream_wrapper_log_error() it is the classic php error. Both were
acceptable and the only way available, without relying on custom
methods, but if the stream's error handling is being improved, or made
future proof (when possible), together with the typed exception, we
could get something good. The typed exceptions give callers a clean
API. Adding an emit function gives wrapper authors a clean API. They
close the loop. Either one without the other leaves the whole thing
incomplete.
If typed exceptions exist and stream_emit_error in userland and
php_stream_emit_error internally exist, wrappers can emit errors that
integrate cleanly with the catch hierarchy, and have better error
reporting.
Some of the future scopes seem foundational to stream error handling
improvements, and risky to delay rather than allowing them now.
the slightly off topic ones:
Is there a published overall design document for the stream evolution
project? The foundation blog post outlines four pillars at a (very)
high level, but the relationship between the RFCs, their
sequencing/order rationale to be added already, and the intended end
state of the streams API are not spelled out anywhere I can find.
Having that available would make it much easier to give a more
informed feedback on each RFC as it arrives, and to flag conflicts or
gaps early.
Streams were introduced over two decades ago (afair it was in the
early 2000s, by Wez. we aren't younger :), and much has changed since.
Async runtimes (swoole, reactphp, FrankenPhp to some extent, etc),
fibers, modern TLS, io_uring, and very different application patterns.
The error RFC itself acknowledges the BC constraints that shape its
design. My concern is that modernizing streams incrementally in 8.x,
under those constraints, risks producing something that is improved
but still not fit for where PHP needs to go, or ideally should go,
particularly around async I/O, where error handling, stream selection,
and the overall API surface will need to work together in a coherent
manner.
Is there a published async I/O architecture that these RFCs are
building toward? The other stream related RFCs or the phpf's blog
post mention future work about async IO, and related stream topics,
which suggests the async design exists internally but hasn't been
shared publicly. If that's the case, it would be fantastic to share it
(even as a draft design doc), so it can be assessed whether the
current incremental steps are pointing in the right direction, or
whether some decisions being made now will need to be undone later (or
will be impossible to undo without breaking BC). Especially if authors
of Swoole (made io uring into their php 8.5 version it seems :),
reactphp, etc would surely provide extremely good feedback too.
Best,
Pierre
@pierrejoye
Hi,
Sorry for slightly late reply. I wanted to think about it and was also busy
with other stuff.
Hi Jakub,
Hello,
I would like to introduce a new stream error handling RFC that is part
of my stream evolution work (PHP Foundation project funded by Sovereign
Tech Fund) :Thank you for this RFC! . It is clearly needed and the error chaining
design is great. There are a few points I'd like to raise, the last
two being slightly off topic specifically to that rfc but however very
related to. So a slightly long reply.
- New exceptions
The current design has a single flat StreamException. This means the
exception path requires inspecting the error object to branch on
category:catch (StreamException $e) {
if ($e->getError()->isNetworkError()) {
// retry
} elseif ($e->getError()->code === StreamError::CODE_NOT_FOUND) {
// fallback
}
}The current design introduces StreamException, which is good. But as a
single flat class it doesn't actually allow to use exceptions the way
exceptions are meant to be used. One still has to inspect the error
object inside the catch block to know what happened. It introduces the
syntax of exception handling without the benefit of it.Since the error categories are already well-defined and stable in this
RFC, StreamException could be a base class with typed subclasses
mirroring those categories:StreamException
├ StreamIoException
├ StreamFileSystemException
├ StreamNetworkException
├ StreamWrapperException
└ ...This allows the more natural and idiomatic:
catch (StreamNetworkException $e) {
// retry — type alone tells you what happened
} catch (StreamNotFoundException $e) {
// fallback
} catch (StreamException $e) {
// generic
}The StreamError value object can stay as in the RFC (and keep "BC" for
the error const's bag), it's appropriate for the inspection/silent
mode use case. I see the typed exception hierarchy and the flat value
object serve different purposes and complement each other:
$e->getError() still gives you full detail when you need it. These are
separable concerns currently merged into one class.
I thought about this a lot and I'm not sure this would be that useful as I
don't see a big enough use case for cetegorization on exception level. It
was exposed more as a nice to have but I doubt that users will care that
much if the error is an Io or FileSystem category. It seems also slightly
messe to have the exception class dependent on the error category that it
holds as a property. I would possibly reconsider it if I saw a big use case
for categorization but I see it more as a nice to have utility which
doesn't need to propagate in this way.
Since StreamException doesn't exist in any current PHP version, there
is no existing userland code catching it. This is a clean state.
Adding a typed hierarchy now costs nothing in BC terms, and
retrofitting it later would be a BC break.
I don't think it would be an unacceptable BC break to split it later if
there was a really need for that. The parent would still
stay StreamException and all code that catches it would still work. I don't
think any reasonable code would relay on the exception to be exactly
StreamException and not it's subclass so I wouldn't see that as an issue
form the BC point of view.
It is more than just syntactic sugar, it allows very clean logic.
static analysis tools can reason about which exceptions a function
throws, IDEs can suggest catch blocks, and retry wrappers can target
network errors specifically without accidentally swallowing filesystem
errors. The cost of adding the hierarchy now is low; retrofitting it
later is a BC break.
As I said above I really don't see much use case in just targeting network
errors. That's really niche use case IMHO and for that it can just get it
from the StreamError. My main issue with this is that dependency between
StreamException subclass and StreamError.
- Custom stream wrapper and errors
How does custom wrappers, be in ext/, or out of core wrappers, exts or
user land, can emit errors? Or is it out of the scope of this RFC?
The wrapper needs to be adapted and call the new API functions. It might
also require additional global types if needed (most of the wrappers should
be fine with existing ones but there might be use case to add more).
For wrappers authors, f.e. in zip, php_error_docref(NULL, E_WARNING,
"Zip stream error: %s", zip_error_strerror(err));, or
php_stream_wrapper_log_error() it is the classic php error. Both were
acceptable and the only way available, without relying on custom
methods, but if the stream's error handling is being improved, or made
future proof (when possible), together with the typed exception, we
could get something good. The typed exceptions give callers a clean
API. Adding an emit function gives wrapper authors a clean API. They
close the loop. Either one without the other leaves the whole thing
incomplete.
If typed exceptions exist and stream_emit_error in userland and
php_stream_emit_error internally exist, wrappers can emit errors that
integrate cleanly with the catch hierarchy, and have better error
reporting.
The internal API is working just with types that fall into some categories.
I don't see any relation to typed exception here. The API is just about
emitting typed errors through.
Some of the future scopes seem foundational to stream error handling
improvements, and risky to delay rather than allowing them now.
Covering all existing wrappers from the start is just not realistic. I
actually started with generic type registration but it got quite complex
and it would require some unstable type registration which couldn't be
exposed as constants. I also realised that the actually types are repeating
between wrappers so it was just better to switch to global types.
That extension type ranges in future scope was actually a left over when I
was still considering this dynamic registration. I just removed it.
the slightly off topic ones:
Is there a published overall design document for the stream evolution
project? The foundation blog post outlines four pillars at a (very)
high level, but the relationship between the RFCs, their
sequencing/order rationale to be added already, and the intended end
state of the streams API are not spelled out anywhere I can find.
Having that available would make it much easier to give a more
informed feedback on each RFC as it arrives, and to flag conflicts or
gaps early.
I got some future improvements in my head but the short term plan is just
what was published and what we have funding for. The rest depends on
further funding and available resources to do the actual work so there is
not much point in too detailed plan.
Streams were introduced over two decades ago (afair it was in the
early 2000s, by Wez. we aren't younger :), and much has changed since.
Async runtimes (swoole, reactphp, FrankenPhp to some extent, etc),
fibers, modern TLS, io_uring, and very different application patterns.
The error RFC itself acknowledges the BC constraints that shape its
design. My concern is that modernizing streams incrementally in 8.x,
under those constraints, risks producing something that is improved
but still not fit for where PHP needs to go, or ideally should go,
particularly around async I/O, where error handling, stream selection,
and the overall API surface will need to work together in a coherent
manner.Is there a published async I/O architecture that these RFCs are
building toward? The other stream related RFCs or the phpf's blog
post mention future work about async IO, and related stream topics,
which suggests the async design exists internally but hasn't been
shared publicly. If that's the case, it would be fantastic to share it
(even as a draft design doc), so it can be assessed whether the
current incremental steps are pointing in the right direction, or
whether some decisions being made now will need to be undone later (or
will be impossible to undo without breaking BC). Especially if authors
of Swoole (made io uring into their php 8.5 version it seems :),
reactphp, etc would surely provide extremely good feedback too.
The polling RFC is the first step that also introduces new IO handle
abstraction. I'm building on that internally in new copy API that also uses
those new handles. What I plan to look after polling is the notification
API that would all IO notifications before the actual IO with future
possibility of taking over the IO operation. Then I'm also experiment with
new IO ring API (basically io_uring with fallbacks for other platforms)
that would allow async IO handling. I don't have more details at this stage
but there should be RFC's coming in the second half of this year where more
details will be shared.
Kind regards,
Jakub
Hi,
Hi,
Sorry for slightly late reply. I wanted to think about it and was also
busy with other stuff.Hi Jakub,
Hello,
I would like to introduce a new stream error handling RFC that is part
of my stream evolution work (PHP Foundation project funded by Sovereign
Tech Fund) :Thank you for this RFC! . It is clearly needed and the error chaining
design is great. There are a few points I'd like to raise, the last
two being slightly off topic specifically to that rfc but however very
related to. So a slightly long reply.
- New exceptions
The current design has a single flat StreamException. This means the
exception path requires inspecting the error object to branch on
category:catch (StreamException $e) {
if ($e->getError()->isNetworkError()) {
// retry
} elseif ($e->getError()->code === StreamError::CODE_NOT_FOUND) {
// fallback
}
}The current design introduces StreamException, which is good. But as a
single flat class it doesn't actually allow to use exceptions the way
exceptions are meant to be used. One still has to inspect the error
object inside the catch block to know what happened. It introduces the
syntax of exception handling without the benefit of it.Since the error categories are already well-defined and stable in this
RFC, StreamException could be a base class with typed subclasses
mirroring those categories:StreamException
├ StreamIoException
├ StreamFileSystemException
├ StreamNetworkException
├ StreamWrapperException
└ ...This allows the more natural and idiomatic:
catch (StreamNetworkException $e) {
// retry — type alone tells you what happened
} catch (StreamNotFoundException $e) {
// fallback
} catch (StreamException $e) {
// generic
}The StreamError value object can stay as in the RFC (and keep "BC" for
the error const's bag), it's appropriate for the inspection/silent
mode use case. I see the typed exception hierarchy and the flat value
object serve different purposes and complement each other:
$e->getError() still gives you full detail when you need it. These are
separable concerns currently merged into one class.I thought about this a lot and I'm not sure this would be that useful as I
don't see a big enough use case for cetegorization on exception level. It
was exposed more as a nice to have but I doubt that users will care that
much if the error is an Io or FileSystem category. It seems also slightly
messe to have the exception class dependent on the error category that it
holds as a property. I would possibly reconsider it if I saw a big use case
for categorization but I see it more as a nice to have utility which
doesn't need to propagate in this way.
When implementing the recent changes I actually realised that introducing
this exception hierarchy does not make any sense because there can be
multiple errors in an exception and they can have different categories so
the exception cannot be reasonably categorised to a single category. This
became more obvious with getErrors() - previously I didn't realised it
because it was linked list and somehow forgot that they can have different
categories.
Kind regards,
Jakub