====== PHP RFC: Stream Error Handling Improvements ======
* Version: 1.0
* 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
* **Semantic error codes** - well-defined codes for different error conditions
===== Proposal =====
This proposal introduces a comprehensive error handling system for PHP streams through new context options, a dedicated
exception class, error codes, and a function to retrieve 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 one of the following constants:
* **STREAM_ERROR_MODE_ERROR** (default) - Use standard PHP error reporting (warnings/notices)
* **STREAM_ERROR_MODE_EXCEPTION** - Throw StreamException for terminal errors
* **STREAM_ERROR_MODE_SILENT** - Suppress all error output
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).
In exception mode, only terminal errors throw exceptions. Non-terminal errors are still stored (if error_store permits)
but don't interrupt execution.
=== error_store ===
Controls which errors are stored for later retrieval via `stream_get_errors()`. Accepts one of:
* **STREAM_ERROR_STORE_AUTO** (default) - Automatically decide based on error_mode:
* With ERROR mode: stores none
* With EXCEPTION mode: stores non-terminal errors
* With SILENT mode: stores all errors
* **STREAM_ERROR_STORE_NONE** - Don't store any errors
* **STREAM_ERROR_STORE_NON_TERMINAL** - Store only non-terminal errors
* **STREAM_ERROR_STORE_TERMINAL** - Store only terminal errors
* **STREAM_ERROR_STORE_ALL** - Store all errors
Stored errors can be retrieved using `stream_get_errors()` with either a stream resource (for stream-specific errors)
or wrapper name (for wrapper-level errors).
=== error_handler ===
Optional callback to receive error notifications. The callback signature is:
function(string $wrapper, ?resource $stream, int $code, string $message, ?string $param): void
Parameters:
* **$wrapper** - The wrapper name (e.g., "file", "http", "PHP")
* **$stream** - The stream resource if available, null for wrapper-level errors
* **$code** - One of the STREAM_ERROR_CODE_* constants
* **$message** - Human-readable error message
* **$param** - Additional context (typically filename or URL)
The error handler is called regardless of the error_mode setting, allowing custom logging or handling logic.
==== API ====
=== stream_get_errors Function ===
=== StreamException Class ===
=== Error Mode Constants ===
=== Error Store Constants ===
=== Error Code Constants ===
Error codes are organized by operation category to make error handling more intuitive:
==== Stream vs Wrapper Errors ====
The error handling system distinguishes between two types of errors:
**Stream Errors** are associated with a specific stream resource and are stored in the stream itself. These errors can be retrieved by passing the stream resource to `stream_get_errors()`.
**Wrapper Errors** occur during wrapper operations that don't result in a stream (e.g., failed `fopen()` attempts, `unlink()`, `rename()`). These are stored globally per wrapper name and can be retrieved by passing the wrapper name to `stream_get_errors()` or by calling it with no arguments to get all wrapper errors.
This separation allows precise error tracking:
- Stream errors: `stream_get_errors($stream)`
- Specific wrapper errors: `stream_get_errors('file')`
- All wrapper errors: `stream_get_errors()`
==== Logged Errors ====
PHP's stream system has an existing mechanism for "logged errors" that are accumulated during certain operations and
displayed together (e.g., when `fopen()` fails). This RFC keeps this functionality but the error displaying is done
through the new error handling so the context options work here as well.
==== Usage Examples ====
=== Exception Mode ===
[
'error_mode' => STREAM_ERROR_MODE_EXCEPTION,
]
]);
try {
$stream = fopen('/nonexistent/file.txt', 'r', false, $context);
} catch (StreamException $e) {
echo "Error: " . $e->getMessage() . "\n";
echo "Code: " . $e->getCode() . "\n";
echo "Wrapper: " . $e->getWrapperName() . "\n";
echo "File: " . $e->getParam() . "\n";
}
?>
=== Silent Mode with Error Storage ===
[
'error_mode' => STREAM_ERROR_MODE_SILENT,
'error_store' => STREAM_ERROR_STORE_ALL,
]
]);
$stream = @fopen('http://example.com/nonexistent', 'r', false, $context);
if ($stream === false) {
$errors = stream_get_errors('http');
foreach ($errors as $error) {
echo "Error {$error['code']}: {$error['message']}\n";
if ($error['code'] === STREAM_ERROR_CODE_NOT_FOUND) {
echo "Resource not found, using fallback...\n";
// Handle 404 specifically
}
}
}
?>
=== Custom Error Handler ===
[
'error_mode' => STREAM_ERROR_MODE_SILENT,
'error_handler' => function($wrapper, $stream, $code, $message, $param) use (&$errorLog) {
$errorLog[] = [
'timestamp' => time(),
'wrapper' => $wrapper,
'code' => $code,
'message' => $message,
'param' => $param,
];
}
]
]);
// 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";
}
?>
=== Retrieving Stream-Specific Errors ===
[
'error_mode' => STREAM_ERROR_MODE_SILENT,
'error_store' => STREAM_ERROR_STORE_ALL,
]
]);
$stream = fopen('php://temp', 'r+', false, $context);
// Perform operations that may generate errors
fwrite($stream, 'test data');
fseek($stream, -100, SEEK_SET); // This may generate a non-terminal error
// Get errors specific to this stream
$errors = stream_get_errors($stream);
foreach ($errors as $error) {
if (!$error['terminal']) {
echo "Warning: {$error['message']}\n";
}
}
fclose($stream);
?>
=== Setting Default Context ===
You can set default error handling for functions that don't accept a context parameter using `stream_context_set_default()`:
[
'error_mode' => STREAM_ERROR_MODE_EXCEPTION,
]
]);
// Now all stream operations use exception mode by default
try {
$contents = file_get_contents('/nonexistent/file.txt');
} catch (StreamException $e) {
echo "Failed to read file: " . $e->getMessage() . "\n";
}
// Or using silent mode for specific operations
$silentContext = stream_context_create([
'stream' => [
'error_mode' => STREAM_ERROR_MODE_SILENT,
]
]);
if (file_get_contents('/another/file.txt', false, $silentContext) === false) {
$errors = stream_get_errors('file');
// Handle errors programmatically
}
?>
===== Backward Incompatible Changes =====
This RFC only adds new functionality and does not modify existing behavior except small changes noted below.
There is one case of error that was incorrectly reported and such warning got dropped. There might be a few extra
warnings that were supposed to be hidden. Those are more fixes however.
Another potentially minor BC break in passing context for opening stream to the new stream. This was done only for
some wrapper (mainly the network related ones). This should be actually expected for all streams but for some reason it
wasn't the case. So such change seems to make sense. However, if this becomes a problem, there is a way to make it
limited to errors or selected options so the impact can be eliminated if needed.
===== 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
* **Error code ranges** - Reserved ranges for extensions to define custom error codes
===== 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
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
===== 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 =====
* 1.0 - Initial version