[RFC] Stream Error Handling Improvements

php.internals

Jakub Zelenka

287 days ago
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

nyamsprod the funky webmaster

286 days ago
On Tue, Nov 18, 2025 at 7:41 PM 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 > > 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

Bob Weinand

286 days ago
Hey Jakub, On 18.11.2025 19:38:37, Jakub Zelenka 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
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

Jakub Zelenka

286 days ago
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 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. Cheers, Jakub

Bob Weinand

286 days ago
Hey Jakub, thanks for the detailed reply. On 19.11.2025 12:52:35, Jakub Zelenka wrote:
> 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

Jakub Zelenka

285 days ago
Hi Bob, On Wed, Nov 19, 2025 at 5:37 PM Bob Weinand <bobwei9@hotmail.com> wrote:
> Hey Jakub, > > thanks for the detailed reply. > On 19.11.2025 12:52:35, Jakub Zelenka wrote: > > 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() 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. >
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

Bob Weinand

285 days ago
Hey Jakub, On 19.11.2025 23:58:57, Jakub Zelenka wrote:
> On Wed, Nov 19, 2025 at 5:37 PM Bob Weinand <bobwei9@hotmail.com> wrote: > > On 19.11.2025 12:52:35, Jakub Zelenka wrote: >> 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

Jakub Zelenka

285 days ago
Hi Bob, On Thu, Nov 20, 2025 at 1:11 AM Bob Weinand <bobwei9@hotmail.com> wrote:
> Hey Jakub, > On 19.11.2025 23:58:57, Jakub Zelenka wrote: > > On Wed, Nov 19, 2025 at 5:37 PM Bob Weinand <bobwei9@hotmail.com> wrote: > >> On 19.11.2025 12:52:35, Jakub Zelenka wrote: >> >> 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

Bob Weinand

284 days ago
Hey Jakub, On 20.11.2025 13:03:08, Jakub Zelenka wrote:
> > 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

Jordi Boggiano

286 days ago
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

Lynn

286 days ago
On Tue, Nov 18, 2025 at 7:40 PM 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 > > 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".

nyamsprod the funky webmaster

286 days ago
On Wed, Nov 19, 2025 at 10:45 AM Lynn <kjarli@gmail.com> wrote:
> > > On Tue, Nov 18, 2025 at 7:40 PM 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 >> >> 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

Lynn

286 days ago
On Wed, Nov 19, 2025 at 10:48 AM ignace nyamagana butera < nyamsprod@gmail.com> wrote:
> > On Wed, Nov 19, 2025 at 10:45 AM Lynn <kjarli@gmail.com> wrote: > >> >> >> On Tue, Nov 18, 2025 at 7:40 PM 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 >>> >>> 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.

Jakub Zelenka

286 days ago
Hi, On Wed, Nov 19, 2025 at 10:57 AM Lynn <kjarli@gmail.com> wrote:
> > > On Wed, Nov 19, 2025 at 10:48 AM ignace nyamagana butera < > nyamsprod@gmail.com> wrote: > >> >> On Wed, Nov 19, 2025 at 10:45 AM Lynn <kjarli@gmail.com> wrote: >> >>> >>> >>> On Tue, Nov 18, 2025 at 7:40 PM 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 >>>> >>>> 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. >
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

Gina P. Banyard

286 days ago
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? 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

Jakub Zelenka

286 days ago
Hi Gina, On Wed, Nov 19, 2025 at 12:14 PM Gina P. Banyard <internals@gpb.moe> wrote:
> 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

Larry Garfield

286 days ago
On Tue, Nov 18, 2025, at 12:38 PM, Jakub Zelenka 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: 1. It's very inconsistent. 2. 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

Jakub Zelenka

285 days ago
Hi, On Wed, Nov 19, 2025 at 5:35 PM Larry Garfield <larry@garfieldtech.com> wrote:
> On Tue, Nov 18, 2025, at 12:38 PM, Jakub Zelenka 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: > > 1. It's very inconsistent. > 2. 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

Arnaud.lb

263 days ago
Hi Jakub, On Tue, Nov 18, 2025 at 7:39 PM 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 > > 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

Jakub Zelenka

245 days ago
On Tue, Nov 18, 2025 at 7:38 PM 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, 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

Weedpacket

245 days ago
On 2025-12-30 07:52, Jakub Zelenka wrote:
> >  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.

Jakub Zelenka

188 days ago
Hi, On Tue, Dec 30, 2025 at 12:25 AM Morgan <weedpacket@varteg.nz> wrote:
> On 2025-12-30 07:52, Jakub Zelenka wrote: > > > > 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

Christian Schneider

245 days ago
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

Jakub Zelenka

188 days ago
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

Jakub Zelenka

188 days ago
Hi, On Tue, Nov 18, 2025 at 7:38 PM 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 > >
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

Nicolas Grekas

188 days ago
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) : >> >> https://wiki.php.net/rfc/stream_errors >> >> > 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: 1. 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? 2. 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? 3. 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. 4. 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

Jakub Zelenka

187 days ago
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) : >>> >>> https://wiki.php.net/rfc/stream_errors >>> >>> >> 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: > > 1. 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
> 2. 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.
> 3. 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. >
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.
> > 4. 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

Nicolas Grekas

187 days ago
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) : >>>> >>>> https://wiki.php.net/rfc/stream_errors >>>> >>>> >>> 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: >> >> 1. 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 > > >> 2. 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. > > >> 3. 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. >> > > 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. > > >> >> 4. 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

Jakub Zelenka

187 days ago
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) : >>>>> >>>>> https://wiki.php.net/rfc/stream_errors >>>>> >>>>> >>>> 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: >>> >>> 1. 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 >> >> >>> 2. 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. >> >> >>> 3. 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. >>> >> >> 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. >> >> >>> >>> 4. 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

Jakub Zelenka

185 days ago
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) : >>> >>> https://wiki.php.net/rfc/stream_errors >>> >>> >>
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: > > 1. 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.
> > 2. 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.
> 3. 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. > >
I changed it to the StreamError class constants and also move the is*Error functions there.
> 4. 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

Nicolas Grekas

185 days ago
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) : >>>> >>>> https://wiki.php.net/rfc/stream_errors >>>> >>>> >>> > 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: >> >> 1. 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. > > >> >> 2. 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. > > >> 3. 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. >> >> > I changed it to the StreamError class constants and also move the is*Error > functions there. > > >> 4. 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

Jakub Zelenka

185 days ago
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) : >>>>> >>>>> https://wiki.php.net/rfc/stream_errors >>>>> >>>>> >>>> >> 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: >>> >>> 1. 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. >> >> >>> >>> 2. 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. >> >> >>> 3. 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. >>> >>> >> I changed it to the StreamError class constants and also move the >> is*Error functions there. >> >> >>> 4. 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

Tim Düsterhus

159 days ago
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

Jakub Zelenka

156 days ago
Hi, On Thu, Mar 26, 2026 at 5:57 PM Tim Düsterhus <tim@bastelstu.be> wrote:
> 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: > >
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 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). > >
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()` 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. > >
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

Tim Düsterhus

154 days ago
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()` 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. >> >> > 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: 1. "// 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. 2. 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 :-) 3. 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

Pierre Joye

152 days ago
Hi Tim, Jakub, On Tue, Mar 31, 2026 at 5:52 PM Tim Düsterhus <tim@bastelstu.be> wrote:
> > 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()` 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. > >> > >> > > 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! >
...
> 3. 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()`
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

Jakub Zelenka

152 days ago
Hi, On Thu, Apr 2, 2026 at 11:18 AM Pierre Joye <pierre.php@gmail.com> wrote:
> Hi Tim, Jakub, > > On Tue, Mar 31, 2026 at 5:52 PM Tim Düsterhus <tim@bastelstu.be> wrote: > > > > 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()` 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. > > >> > > >> > > > 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! > > > ... > > 3. 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()` > > 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

Jakub Zelenka

152 days ago
Hi On Tue, Mar 31, 2026 at 11:59 AM Tim Düsterhus <tim@bastelstu.be> wrote:
> > 1. "// 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
> 2. 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
> 3. 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. > >
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

Tim Düsterhus

151 days ago
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

Pierre Joye

151 days ago
hi, On Fri, Apr 3, 2026, 3:52 AM Tim Düsterhus <tim@bastelstu.be> wrote:
> 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

Pierre Joye

184 days ago
Hi Jakub, On Tue, Dec 23, 2025 at 6:10 AM 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
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. 1. 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. 2. 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

Jakub Zelenka

162 days ago
Hi, Sorry for slightly late reply. I wanted to think about it and was also busy with other stuff. On Sun, Mar 1, 2026 at 4:09 PM Pierre Joye <pierre.php@gmail.com> wrote:
> Hi Jakub, > > On Tue, Dec 23, 2025 at 6:10 AM 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 > > 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. > > 1. 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.
> > 2. 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

Jakub Zelenka

156 days ago
Hi, On Sun, Mar 22, 2026 at 8:26 PM Jakub Zelenka <bukka@php.net> wrote:
> Hi, > > Sorry for slightly late reply. I wanted to think about it and was also > busy with other stuff. > > On Sun, Mar 1, 2026 at 4:09 PM Pierre Joye <pierre.php@gmail.com> wrote: > >> Hi Jakub, >> >> On Tue, Dec 23, 2025 at 6:10 AM 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 >> >> 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. >> >> 1. 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

Jakub Zelenka

123 days ago
Hi, On Tue, Nov 18, 2025 at 7:38 PM 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 > >
Just a heads up that I plan to open voting on this on Sun 3rd May. Cheers Jakub