[PHP-DEV] [RFC] Stream Error Handling Improvements

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.

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
  PHP: rfc:stream_errors
looks a bit clunky to me and I was wondering if returning an array of StreamError would not be more straightforward, i.e. one could then use foreach, empty(), count(), array_first(), array_last(), array_find() etc. instead of methods like while, $e->count(), $e->hasCode() which would make it feel more idiomatic to me.

But then again it might be a matter of taste and/or I might have missed something.

Regards,
- Chris

Hi,

On Tue, Dec 30, 2025 at 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

Hi,

On Tue, Dec 30, 2025 at 9:24 AM Christian Schneider <cschneid@cschneid.com> wrote:

Am 29.12.2025 um 19:52 schrieb Jakub Zelenka <bukka@php.net>:

  • The storing of errors is done at the end of the operation grouping and contains only the last grouped errors. So the function name was changed to stream_get_last_error(). It returns just a single StreamError which has a next property pointing to the next error if there is any.

I assume the chaining was based on Exception chaining but the example code under
https://wiki.php.net/rfc/stream_errors#error_chaining
looks a bit clunky to me and I was wondering if returning an array of StreamError would not be more straightforward, i.e. one could then use foreach, empty(), count(), array_first(), array_last(), array_find() etc. instead of methods like while, $e->count(), $e->hasCode() which would make it feel more idiomatic to me.

I thought about it and I think the exception like chaining makes a bit more sense here. The thing is that in majority cases there will be only one error so it would be most of the time array with just one element which seems a bit wasteful. But if others also feel that array would be better, I wouldn’t mind to do follow up RFC as I don’t really feel strongly about it. Just let me know during the vote and if there’s more people asking for it, I will do a follow up RFC. Alternatively it could just implement iterator or array access, which I was thinking about, but not sure if it’s not too messy.

Kind regards,

Jakub

Hi,

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

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?

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

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

  1. Using stream_context_set_default to change error_mode looks hazardous

The RFC includes an example where stream_context_set_default is used to set error_mode to StreamErrorMode::Exception globally. I’m worried about the ecosystem impact here: if any library or application bootstrap does use this, then existing packages using the common @file_get_contents(‘maybe_existing_file’) idiom could e.g. suddenly throw uncaught exceptions, breaking behavior their authors had deliberately chosen. This feels like a significant compatibility hazard for code that doesn’t control its full execution environment.

Would it be worth restricting error_mode (and possibly the other new options) so that they can only be set via per-call contexts, not via stream_context_set_default?

Thanks for working on this, we definitely need this!

Cheers,
Nicolas

Hi,

On Wed, Feb 25, 2026 at 7:11 PM Nicolas Grekas <nicolas.grekas+php@gmail.com> wrote:

Hi Jakub,

I would like to introduce a new stream error handling RFC that is part of my stream evolution work (PHP Foundation project funded by Sovereign Tech Fund) :

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

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

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

  1. Using stream_context_set_default to change error_mode looks hazardous

The RFC includes an example where stream_context_set_default is used to set error_mode to StreamErrorMode::Exception globally. I’m worried about the ecosystem impact here: if any library or application bootstrap does use this, then existing packages using the common @file_get_contents(‘maybe_existing_file’) idiom could e.g. suddenly throw uncaught exceptions, breaking behavior their authors had deliberately chosen. This feels like a significant compatibility hazard for code that doesn’t control its full execution environment.

Would it be worth restricting error_mode (and possibly the other new options) so that they can only be set via per-call contexts, not via stream_context_set_default?

Hmm that’s how stream context works and it would seem quite hacky to add restriction on global context (we would basically need to add some validation when global context is set). What’s worse is that it would prevent to change error handling for cases where it is not possible to set context (fsockopen and various other cases). This is actually in some way already possible by throwing from the stream notifications (limited to http wrapper though) but I can see how it could have bigger impact here. I’m afraid the libraries will have to deal with that which should eventually lead to a better code.

Cheers,

Jakub

Le mer. 25 févr. 2026 à 20:16, Jakub Zelenka <bukka@php.net> a écrit :

Hi,

On Wed, Feb 25, 2026 at 7:11 PM Nicolas Grekas <nicolas.grekas+php@gmail.com> wrote:

Hi Jakub,

I would like to introduce a new stream error handling RFC that is part of my stream evolution work (PHP Foundation project funded by Sovereign Tech Fund) :

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

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

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

  1. Using stream_context_set_default to change error_mode looks hazardous

The RFC includes an example where stream_context_set_default is used to set error_mode to StreamErrorMode::Exception globally. I’m worried about the ecosystem impact here: if any library or application bootstrap does use this, then existing packages using the common @file_get_contents(‘maybe_existing_file’) idiom could e.g. suddenly throw uncaught exceptions, breaking behavior their authors had deliberately chosen. This feels like a significant compatibility hazard for code that doesn’t control its full execution environment.

Would it be worth restricting error_mode (and possibly the other new options) so that they can only be set via per-call contexts, not via stream_context_set_default?

Hmm that’s how stream context works and it would seem quite hacky to add restriction on global context (we would basically need to add some validation when global context is set). What’s worse is that it would prevent to change error handling for cases where it is not possible to set context (fsockopen and various other cases). This is actually in some way already possible by throwing from the stream notifications (limited to http wrapper though) but I can see how it could have bigger impact here. I’m afraid the libraries will have to deal with that which should eventually lead to a better code.

Thanks for acknowledging the first items, I’m looking forward to the update.

About this last one, I need to insist: that global behavior is going to be a nightmare. Everytime someone proposes to add a new ini setting to configure some global behavior, we say so. The reasons are exactly the same. Existing code will just have to be rewritten, which might lead to better code but also to high friction. Exactly like a BC break - it’ll be one actually.

About adding validation to stream_context_set_default, that looks like a non issue to me.
About fsockopen et al, that’s a very good reason to add a context argument. Without that, properly using those function would mean changing the global stat all the time. Better not plan for this.

Cheers,
Nicolas

Hi,

On Thu, Feb 26, 2026 at 7:56 AM Nicolas Grekas <nicolas.grekas+php@gmail.com> wrote:

Le mer. 25 févr. 2026 à 20:16, Jakub Zelenka <bukka@php.net> a écrit :

Hi,

On Wed, Feb 25, 2026 at 7:11 PM Nicolas Grekas <nicolas.grekas+php@gmail.com> wrote:

Hi Jakub,

I would like to introduce a new stream error handling RFC that is part of my stream evolution work (PHP Foundation project funded by Sovereign Tech Fund) :

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

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

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

  1. Using stream_context_set_default to change error_mode looks hazardous

The RFC includes an example where stream_context_set_default is used to set error_mode to StreamErrorMode::Exception globally. I’m worried about the ecosystem impact here: if any library or application bootstrap does use this, then existing packages using the common @file_get_contents(‘maybe_existing_file’) idiom could e.g. suddenly throw uncaught exceptions, breaking behavior their authors had deliberately chosen. This feels like a significant compatibility hazard for code that doesn’t control its full execution environment.

Would it be worth restricting error_mode (and possibly the other new options) so that they can only be set via per-call contexts, not via stream_context_set_default?

Hmm that’s how stream context works and it would seem quite hacky to add restriction on global context (we would basically need to add some validation when global context is set). What’s worse is that it would prevent to change error handling for cases where it is not possible to set context (fsockopen and various other cases). This is actually in some way already possible by throwing from the stream notifications (limited to http wrapper though) but I can see how it could have bigger impact here. I’m afraid the libraries will have to deal with that which should eventually lead to a better code.

Thanks for acknowledging the first items, I’m looking forward to the update.

About this last one, I need to insist: that global behavior is going to be a nightmare. Everytime someone proposes to add a new ini setting to configure some global behavior, we say so. The reasons are exactly the same. Existing code will just have to be rewritten, which might lead to better code but also to high friction. Exactly like a BC break - it’ll be one actually.

About adding validation to stream_context_set_default, that looks like a non issue to me.
About fsockopen et al, that’s a very good reason to add a context argument. Without that, properly using those function would mean changing the global stat all the time. Better not plan for this.

Ok I think you are right that the BC impact would be just too big so I will add that validation.

Kind regards,

Jakub

Hi,

On Wed, Feb 25, 2026 at 7:11 PM Nicolas Grekas <nicolas.grekas+php@gmail.com> wrote:

Hi Jakub,

I would like to introduce a new stream error handling RFC that is part of my stream evolution work (PHP Foundation project funded by Sovereign Tech Fund) :

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.

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

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

  1. Using stream_context_set_default to change error_mode looks hazardous

The RFC includes an example where stream_context_set_default is used to set error_mode to StreamErrorMode::Exception globally. I’m worried about the ecosystem impact here: if any library or application bootstrap does use this, then existing packages using the common @file_get_contents(‘maybe_existing_file’) idiom could e.g. suddenly throw uncaught exceptions, breaking behavior their authors had deliberately chosen. This feels like a significant compatibility hazard for code that doesn’t control its full execution environment.

Would it be worth restricting error_mode (and possibly the other new options) so that they can only be set via per-call contexts, not via stream_context_set_default?

I added that restriction and also added context to some stream functions so it can be set explicitly. There will be more function extended in the future if this passes.

Hope it’s ok now! If there’s anything else, please let me know.

Cheers

Jakub

Le ven. 27 févr. 2026, 22:21, Jakub Zelenka <bukka@php.net> a écrit :

Hi,

On Wed, Feb 25, 2026 at 7:11 PM Nicolas Grekas <nicolas.grekas+php@gmail.com> wrote:

Hi Jakub,

I would like to introduce a new stream error handling RFC that is part of my stream evolution work (PHP Foundation project funded by Sovereign Tech Fund) :

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.

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

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

  1. Using stream_context_set_default to change error_mode looks hazardous

The RFC includes an example where stream_context_set_default is used to set error_mode to StreamErrorMode::Exception globally. I’m worried about the ecosystem impact here: if any library or application bootstrap does use this, then existing packages using the common @file_get_contents(‘maybe_existing_file’) idiom could e.g. suddenly throw uncaught exceptions, breaking behavior their authors had deliberately chosen. This feels like a significant compatibility hazard for code that doesn’t control its full execution environment.

Would it be worth restricting error_mode (and possibly the other new options) so that they can only be set via per-call contexts, not via stream_context_set_default?

I added that restriction and also added context to some stream functions so it can be set explicitly. There will be more function extended in the future if this passes.

Hope it’s ok now! If there’s anything else, please let me know.

Looks nice thanks!

I’d just explicitly tell what happens when one tries to change the error mode globally. An exception? Which one?

Cheers,

Nicolas

On Fri, Feb 27, 2026 at 11:34 PM Nicolas Grekas <nicolas.grekas+php@gmail.com> wrote:

Le ven. 27 févr. 2026, 22:21, Jakub Zelenka <bukka@php.net> a écrit :

Hi,

On Wed, Feb 25, 2026 at 7:11 PM Nicolas Grekas <nicolas.grekas+php@gmail.com> wrote:

Hi Jakub,

I would like to introduce a new stream error handling RFC that is part of my stream evolution work (PHP Foundation project funded by Sovereign Tech Fund) :

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.

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

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

  1. Using stream_context_set_default to change error_mode looks hazardous

The RFC includes an example where stream_context_set_default is used to set error_mode to StreamErrorMode::Exception globally. I’m worried about the ecosystem impact here: if any library or application bootstrap does use this, then existing packages using the common @file_get_contents(‘maybe_existing_file’) idiom could e.g. suddenly throw uncaught exceptions, breaking behavior their authors had deliberately chosen. This feels like a significant compatibility hazard for code that doesn’t control its full execution environment.

Would it be worth restricting error_mode (and possibly the other new options) so that they can only be set via per-call contexts, not via stream_context_set_default?

I added that restriction and also added context to some stream functions so it can be set explicitly. There will be more function extended in the future if this passes.

Hope it’s ok now! If there’s anything else, please let me know.

Looks nice thanks!

I’d just explicitly tell what happens when one tries to change the error mode globally. An exception? Which one?

Ok updated it - it’s ValueError…

Cheers

Jakub

Hi 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) :

PHP: 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

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.

  1. Custom stream wrapper and errors

How does custom wrappers, be in ext/, or out of core wrappers, exts or
user land, can emit errors? Or is it out of the scope of this RFC?

The wrapper needs to be adapted and call the new API functions. It might also require additional global types if needed (most of the wrappers should be fine with existing ones but there might be use case to add more).

For wrappers authors, f.e. in zip, php_error_docref(NULL, E_WARNING,
“Zip stream error: %s”, zip_error_strerror(err));, or
php_stream_wrapper_log_error() it is the classic php error. Both were
acceptable and the only way available, without relying on custom
methods, but if the stream’s error handling is being improved, or made
future proof (when possible), together with the typed exception, we
could get something good. The typed exceptions give callers a clean
API. Adding an emit function gives wrapper authors a clean API. They
close the loop. Either one without the other leaves the whole thing
incomplete.

If typed exceptions exist and stream_emit_error in userland and
php_stream_emit_error internally exist, wrappers can emit errors that
integrate cleanly with the catch hierarchy, and have better error
reporting.

The internal API is working just with types that fall into some categories. I don’t see any relation to typed exception here. The API is just about emitting typed errors through.

Some of the future scopes seem foundational to stream error handling
improvements, and risky to delay rather than allowing them now.

Covering all existing wrappers from the start is just not realistic. I actually started with generic type registration but it got quite complex and it would require some unstable type registration which couldn’t be exposed as constants. I also realised that the actually types are repeating between wrappers so it was just better to switch to global types.

That extension type ranges in future scope was actually a left over when I was still considering this dynamic registration. I just removed it.

the slightly off topic ones:

Is there a published overall design document for the stream evolution
project? The foundation blog post outlines four pillars at a (very)
high level, but the relationship between the RFCs, their
sequencing/order rationale to be added already, and the intended end
state of the streams API are not spelled out anywhere I can find.
Having that available would make it much easier to give a more
informed feedback on each RFC as it arrives, and to flag conflicts or
gaps early.

I got some future improvements in my head but the short term plan is just what was published and what we have funding for. The rest depends on further funding and available resources to do the actual work so there is not much point in too detailed plan.

Streams were introduced over two decades ago (afair it was in the
early 2000s, by Wez. we aren’t younger :), and much has changed since.
Async runtimes (swoole, reactphp, FrankenPhp to some extent, etc),
fibers, modern TLS, io_uring, and very different application patterns.
The error RFC itself acknowledges the BC constraints that shape its
design. My concern is that modernizing streams incrementally in 8.x,
under those constraints, risks producing something that is improved
but still not fit for where PHP needs to go, or ideally should go,
particularly around async I/O, where error handling, stream selection,
and the overall API surface will need to work together in a coherent
manner.

Is there a published async I/O architecture that these RFCs are
building toward? The other stream related RFCs or the phpf’s blog
post mention future work about async IO, and related stream topics,
which suggests the async design exists internally but hasn’t been
shared publicly. If that’s the case, it would be fantastic to share it
(even as a draft design doc), so it can be assessed whether the
current incremental steps are pointing in the right direction, or
whether some decisions being made now will need to be undone later (or
will be impossible to undo without breaking BC). Especially if authors
of Swoole (made io uring into their php 8.5 version it seems :),
reactphp, etc would surely provide extremely good feedback too.

The polling RFC is the first step that also introduces new IO handle abstraction. I’m building on that internally in new copy API that also uses those new handles. What I plan to look after polling is the notification API that would all IO notifications before the actual IO with future possibility of taking over the IO operation. Then I’m also experiment with new IO ring API (basically io_uring with fallbacks for other platforms) that would allow async IO handling. I don’t have more details at this stage but there should be RFC’s coming in the second half of this year where more details will be shared.

Kind regards,

Jakub

Hi

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: ErrorKind in std::io - Rust. Note how the documentation says:

In application code, use match for the ErrorKind values you are expecting; use _ to match “all other errors”.

And the same would be possible in PHP by using a `default` branch in a `match()` expression. We could also add a “NonExhaustiveEnum” interface (or attribute) to PHP as a helper indicator for static analysis tools to warn if a `default` match is missing.

--------

As for the `StreamError` class being a linked list: I agree with the folks that mentioned that the returned errors should be an array instead. `count()` would naturally be available, `hasCode()` can be implemented with `array_any($errors, fn ($error) => $error->code === CODE_DECODING_FAILED)` and the “primary error” can be obtained with `array_first()` (which we added in PHP 8.5).

For the naming of `stream_get_last_error()`: Within PHP we have both `X_get_last_error()` and `X_last_error()`. The latter seems to be more common and also what I would prefer here, because the `stream_get_` prefix sounds to me like we would get something from a stream, but the returned value is not related to a specific stream, but rather a global.

Best regards
Tim Düsterhus

Hi,

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

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

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

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

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

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

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

Fixed

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