Hi Jakub,

On Tue, Dec 23, 2025 at 6:10 AM Jakub Zelenka <[email protected]> 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

Reply via email to