====== PHP RFC: Stream Error Handling Improvements ======
* Version: 2.1
* Date: 2025-11-18
* Author: Jakub Zelenka, bukka@php.net
* Status: Under Discussion
===== Introduction =====
PHP's stream error handling has historically been inconsistent across different stream wrappers and operations. Errors
are reported through various mechanisms - some use warnings, others use notices, and the error messages often lack
structure or context. This makes it difficult to:
* Handle errors programmatically without relying on error handlers
* Distinguish between different types of errors
* Retrieve error information after operations complete
* Build robust applications that need fine-grained error control
This RFC proposes a unified error handling system for PHP streams that provides:
* **Consistent error reporting** across all stream wrappers
* **Multiple error modes** - traditional warnings, exceptions, or silent mode
* **Structured error storage** - retrieve detailed error information programmatically
* **Custom error handlers** - error callback support with structured error objects
* **Semantic error codes** - well-defined integer constants for different error conditions
* **Error chaining** - track multiple related errors in a single operation
===== Proposal =====
This proposal introduces a comprehensive error handling system for PHP streams through new context options, enums,
classes, and functions for retrieving stored errors.
==== Stream Error Context Options ====
Three new context options are added under the root `stream` field to control error handling behavior:
=== error_mode ===
Controls how errors are reported. Accepts a `StreamErrorMode` enum value:
* **StreamErrorMode::Error** (default) - Use standard PHP error reporting (warnings/notices)
* **StreamErrorMode::Exception** - Throw StreamException for terminating errors
* **StreamErrorMode::Silent** - Suppress all error output
Terminating errors are those that prevent an operation from completing (e.g., file not found, permission denied).
Non-terminating errors are warnings or notices that don't stop execution (e.g., buffer truncation).
In exception mode, only terminating errors throw exceptions. Non-terminating errors are still stored (if error_store
permits) but don't interrupt execution.
Error handling options (`error_mode` and `error_store`) cannot be set on the default context via
`stream_context_set_default()`. This restriction prevents backward compatibility issues with libraries that rely on
stream warnings being emitted in the default configuration. Error handling must always be configured through an
explicit context created with `stream_context_create()`. If `stream_context_set_default()` is used for setting
`error_mode`, `error_store` or `error_handler`, the `ValueError` exception is thrown.
=== error_store ===
Controls which errors are stored for later retrieval via `stream_get_last_error()`. Accepts a `StreamErrorStore` enum
value:
* **StreamErrorStore::Auto** (default) - Automatically decide based on error_mode:
* With Error mode: stores none
* With Exception mode: stores non-terminating errors
* With Silent mode: stores all errors
* **StreamErrorStore::None** - Don't store any errors
* **StreamErrorStore::NonTerminating** - Store only non-terminating errors
* **StreamErrorStore::Terminating** - Store only terminating errors
* **StreamErrorStore::All** - Store all errors
Stored errors can be retrieved using `stream_get_last_error()` which returns the most recent error operation.
=== error_handler ===
Optional callback to receive error notifications. The callback receives a single `StreamError` object:
function(StreamError $error): void
The error handler is called regardless of the error_mode setting, allowing custom logging or handling logic.
==== Context Parameter Additions ====
Several stream functions that previously had no way to specify a stream context now accept an optional `$context`
parameter. This allows error handling options to be used with these functions:
* **stream_select()** - new `$context` parameter (6th argument)
* **stream_copy_to_stream()** - new `$context` parameter (5th argument)
* **stream_socket_pair()** - new `$context` parameter (4th argument)
* **stream_is_local()** - new `$context` parameter (2nd argument)
The `$context` parameter is optional and defaults to `null` (no context). When provided, errors from these functions
are reported according to the context's error handling configuration.
==== API ====
=== StreamErrorMode Enum ===
=== StreamErrorStore Enum ===
=== StreamError Class ===
=== StreamException Class ===
=== stream_get_last_error Function ===
==== Error Codes ====
Error codes are defined as integer class constants on `StreamError` with the `CODE_` prefix. They are organized into
ranges by category:
^ Range ^ Category ^ Example Constants ^
| 0-1 | General | `CODE_NONE`, `CODE_GENERIC` |
| 10-29 | I/O operations | `CODE_READ_FAILED`, `CODE_WRITE_FAILED` |
| 30-69 | File system operations | `CODE_NOT_FOUND`, `CODE_PERMISSION_DENIED` |
| 70-89 | Wrapper/protocol operations | `CODE_NOT_IMPLEMENTED`, `CODE_WRAPPER_NOT_FOUND` |
| 90-99 | Filter operations | `CODE_FILTER_NOT_FOUND`, `CODE_FILTER_FAILED` |
| 100-109 | Cast/conversion operations | `CODE_CAST_FAILED`, `CODE_CAST_NOT_SUPPORTED` |
| 110-129 | Network/socket operations | `CODE_NETWORK_SEND_FAILED`, `CODE_PROTOCOL_ERROR` |
| 130-139 | Encoding/decoding operations | `CODE_ENCODING_FAILED`, `CODE_DECODING_FAILED` |
| 140-149 | Resource/allocation operations| `CODE_ALLOCATION_FAILED`, `CODE_TEMPORARY_FILE_FAILED` |
| 150-159 | Locking operations | `CODE_LOCK_FAILED`, `CODE_LOCK_NOT_SUPPORTED` |
| 160-169 | Userspace stream operations | `CODE_USERSPACE_NOT_IMPLEMENTED` |
The `is*Error()` convenience methods on `StreamError` check whether the error's code falls within the corresponding
range.
==== Error Chaining ====
When a stream operation involves multiple sub-operations (e.g., `stream_select()` calling `stream_cast()` which may
fail), all errors are captured in a single operation and chained together through the `StreamError::$next` property.
The chain is ordered from most significant to least significant: the primary error that caused the operation to fail
is returned directly, and follow-up errors from sub-operations are accessible through `$next`. This is the opposite
of `Exception::$previous` which chains from effect to cause. The rationale is that for stream errors, the first error
is almost always the most useful for error handling, and subsequent errors are linked for completeness but are rarely
needed in practice. Using a linked list rather than an array reinforces this design - the primary error is immediately
available without any array access, and the chain can be traversed when the additional context is needed.
For example, a failed `stream_select()` on a userspace stream might generate:
1. A non-terminating error from reading too much data
2. A terminating error that `stream_cast()` is not implemented
3. A terminating error that the stream type cannot be represented as a descriptor
All three errors are linked together and can be accessed by iterating through the chain:
message . "\n";
$error = $error->next;
}
// Or count them
$totalErrors = $error->count();
// Or search for specific codes
if ($error->hasCode(StreamError::CODE_NOT_IMPLEMENTED)) {
echo "Stream does not implement required functionality\n";
}
?>
==== Usage Examples ====
=== Exception Mode ===
[
'error_mode' => StreamErrorMode::Exception,
]
]);
try {
$stream = fopen('/nonexistent/file.txt', 'r', false, $context);
} catch (StreamException $e) {
echo "Error: " . $e->getMessage() . "\n";
echo "Code: " . $e->getCode() . "\n";
$error = $e->getError();
if ($error) {
echo "Wrapper: " . $error->wrapperName . "\n";
echo "Error Code: " . $error->code . "\n";
if ($error->param) {
echo "File: " . $error->param . "\n";
}
}
}
?>
=== Silent Mode with Error Storage ===
[
'error_mode' => StreamErrorMode::Silent,
'error_store' => StreamErrorStore::All,
]
]);
$stream = @fopen('http://example.com/nonexistent', 'r', false, $context);
if ($stream === false) {
$error = stream_get_last_error();
if ($error) {
echo "Error {$error->code}: {$error->message}\n";
if ($error->code === StreamError::CODE_NOT_FOUND) {
echo "Resource not found, using fallback...\n";
}
// Check error category
if ($error->isNetworkError()) {
echo "Network-related error occurred\n";
}
}
}
?>
=== Custom Error Handler ===
[
'error_mode' => StreamErrorMode::Silent,
'error_handler' => function(StreamError $error) use (&$errorLog) {
$errorLog[] = [
'timestamp' => time(),
'wrapper' => $error->wrapperName,
'code' => $error->code,
'message' => $error->message,
'param' => $error->param,
'count' => $error->count(),
];
}
]
]);
// Attempt multiple file operations
$files = ['file1.txt', 'file2.txt', 'file3.txt'];
foreach ($files as $file) {
$stream = fopen($file, 'r', false, $context);
if ($stream !== false) {
fclose($stream);
}
}
// Review all errors that occurred
foreach ($errorLog as $entry) {
echo "[{$entry['timestamp']}] {$entry['wrapper']}: {$entry['message']}\n";
if ($entry['count'] > 1) {
echo " (with {$entry['count']} total errors)\n";
}
}
?>
=== Processing Error Chains ===
[
'error_mode' => StreamErrorMode::Silent,
'error_store' => StreamErrorStore::All,
]
]);
// This may generate multiple chained errors
$read = [$userStream];
$write = NULL;
$except = NULL;
stream_select($read, $write, $except, 0, 0, $context);
$error = stream_get_last_error();
if ($error) {
// Process all errors in the chain
$current = $error;
while ($current) {
echo "- [{$current->code}]: {$current->message}\n";
$current = $current->next;
}
// Or use helper methods
echo "Total errors: " . $error->count() . "\n";
if ($error->hasCode(StreamError::CODE_CAST_NOT_SUPPORTED)) {
echo "Stream cannot be used with select()\n";
}
}
?>
=== Using Context with stream_copy_to_stream ===
[
'error_mode' => StreamErrorMode::Exception,
]
]);
try {
$src = fopen('source.txt', 'r', false, $context);
$dst = fopen('dest.txt', 'w', false, $context);
stream_copy_to_stream($src, $dst, null, 0, $context);
} catch (StreamException $e) {
echo "Copy failed: " . $e->getMessage() . "\n";
$error = $e->getError();
if ($error && $error->isIoError()) {
echo "I/O error during copy\n";
}
}
?>
===== Backward Incompatible Changes =====
This RFC adds new functionality and maintains backward compatibility with existing code. The default behavior
(StreamErrorMode::Error) preserves all existing error reporting.
Minor changes:
* Some incorrectly reported errors have been fixed
* Context is now properly passed to child streams for consistency
* Error reporting is delayed closer to the function return which might result in re-ordering of normal errors when combined with non stream errors
These changes improve correctness and should not negatively impact existing applications.
===== Proposed PHP Version(s) =====
PHP 8.6
===== Future Scope =====
This proposal establishes a foundation for future enhancements:
* **Additional error codes** - As new stream operations and wrappers are added
* **Extension-specific error ranges** - Reserved ranges for extensions to define custom error codes
* **Error metadata** - Additional context fields for specific error types
* **Context parameter for more functions** - Additional stream functions may gain optional `$context` parameters to enable error handling configuration where currently not possible
===== Proposed Voting Choices =====
As per the voting RFC, a yes/no vote with a 2/3 majority is needed for this proposal to be accepted.
* Yes
* No
* Abstain
The vote started on 2025-XX-XX at XX:XX UTC and ends on 2025-XX-XX at XX:XX UTC.
===== Patches and Tests =====
A working implementation is at https://github.com/php/php-src/pull/20524 .
The implementation requires some further conversions and addition of operation grouping blocks across the codebase.
Some parts are already converted but more will come. Those are trivial changes and they just mean that old errors are
kept in some places or errors might not be grouped.
===== Implementation =====
After the project is implemented, this section will contain:
- the version(s) it was merged to
- a link to the git commit(s)
===== References =====
* PHP Stream Documentation: https://www.php.net/manual/en/book.stream.php
* Stream Context Options: https://www.php.net/manual/en/context.php
* Error Handling in PHP: https://www.php.net/manual/en/language.errors.php
===== Changelog =====
* 2.1 - Replace StreamErrorCode enum with StreamError class constants (CODE_* prefix), move is*Error() methods to StreamError, prohibit error handling options on default context, add context parameter to some stream functions
* 2.0 - Major API redesign with enums, error chaining, and simplified retrieval
* 1.0 - Initial version