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:
This RFC proposes a unified error handling system for PHP streams that provides:
This proposal introduces a comprehensive error handling system for PHP streams through new context options, enums, classes, and functions for retrieving stored errors.
Three new context options are added under the root `stream` field to control error handling behavior:
Controls how errors are reported. Accepts a `StreamErrorMode` enum value:
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.
Controls which errors are stored for later retrieval via `stream_get_last_error()`. Accepts a `StreamErrorStore` enum value:
Stored errors can be retrieved using `stream_get_last_error()` which returns the most recent error operation.
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.
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:
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.
<?php /** * Controls how stream errors are reported */ enum StreamErrorMode { /** * Use standard PHP error reporting (warnings, notices) * This is the default mode and maintains backward compatibility. */ case Error; /** * Throw StreamException for terminating errors * Non-terminating errors are still reported according to error_store setting. */ case Exception; /** * Suppress all error output * Errors can still be stored and retrieved programmatically. */ case Silent; } ?>
<?php /** * Controls which stream errors are stored for retrieval */ enum StreamErrorStore { /** * Automatically decide storage based on error_mode * - Error mode: stores none * - Exception mode: stores non-terminating errors * - Silent mode: stores all errors */ case Auto; /** * Don't store any errors */ case None; /** * Store only non-terminating errors */ case NonTerminating; /** * Store only terminating errors */ case Terminating; /** * Store all errors (both terminating and non-terminating) */ case All; } ?>
<?php /** * Represents a stream error with full context and chaining support * * StreamError objects are immutable and form a linked list through the $next property, * allowing multiple related errors from a single operation to be represented. */ final readonly class StreamError { // General errors const int CODE_NONE = 0; const int CODE_GENERIC = 1; // I/O operation errors (10-29) const int CODE_READ_FAILED = 10; const int CODE_WRITE_FAILED = 11; const int CODE_SEEK_FAILED = 12; const int CODE_SEEK_NOT_SUPPORTED = 13; const int CODE_FLUSH_FAILED = 14; const int CODE_TRUNCATE_FAILED = 15; const int CODE_CONNECT_FAILED = 16; const int CODE_BIND_FAILED = 17; const int CODE_LISTEN_FAILED = 18; const int CODE_NOT_WRITABLE = 19; const int CODE_NOT_READABLE = 20; // File system operation errors (30-69) const int CODE_DISABLED = 30; const int CODE_NOT_FOUND = 31; const int CODE_PERMISSION_DENIED = 32; const int CODE_ALREADY_EXISTS = 33; const int CODE_INVALID_PATH = 34; const int CODE_PATH_TOO_LONG = 35; const int CODE_OPEN_FAILED = 36; const int CODE_CREATE_FAILED = 37; const int CODE_DUP_FAILED = 38; const int CODE_UNLINK_FAILED = 39; const int CODE_RENAME_FAILED = 40; const int CODE_MKDIR_FAILED = 41; const int CODE_RMDIR_FAILED = 42; const int CODE_STAT_FAILED = 43; const int CODE_META_FAILED = 44; const int CODE_CHMOD_FAILED = 45; const int CODE_CHOWN_FAILED = 46; const int CODE_COPY_FAILED = 47; const int CODE_TOUCH_FAILED = 48; const int CODE_INVALID_MODE = 49; const int CODE_INVALID_META = 50; const int CODE_MODE_NOT_SUPPORTED = 51; const int CODE_READONLY = 52; const int CODE_RECURSION_DETECTED = 53; // Wrapper/protocol errors (70-89) const int CODE_NOT_IMPLEMENTED = 70; const int CODE_NO_OPENER = 71; const int CODE_PERSISTENT_NOT_SUPPORTED = 72; const int CODE_WRAPPER_NOT_FOUND = 73; const int CODE_WRAPPER_DISABLED = 74; const int CODE_PROTOCOL_UNSUPPORTED = 75; const int CODE_WRAPPER_REGISTRATION_FAILED = 76; const int CODE_WRAPPER_UNREGISTRATION_FAILED = 77; const int CODE_WRAPPER_RESTORATION_FAILED = 78; // Filter errors (90-99) const int CODE_FILTER_NOT_FOUND = 90; const int CODE_FILTER_FAILED = 91; // Cast/conversion errors (100-109) const int CODE_CAST_FAILED = 100; const int CODE_CAST_NOT_SUPPORTED = 101; const int CODE_MAKE_SEEKABLE_FAILED = 102; const int CODE_BUFFERED_DATA_LOST = 103; // Network/socket errors (110-129) const int CODE_NETWORK_SEND_FAILED = 110; const int CODE_NETWORK_RECV_FAILED = 111; const int CODE_SSL_NOT_SUPPORTED = 112; const int CODE_RESUMPTION_FAILED = 113; const int CODE_SOCKET_PATH_TOO_LONG = 114; const int CODE_OOB_NOT_SUPPORTED = 115; const int CODE_PROTOCOL_ERROR = 116; const int CODE_INVALID_URL = 117; const int CODE_INVALID_RESPONSE = 118; const int CODE_INVALID_HEADER = 119; const int CODE_INVALID_PARAM = 120; const int CODE_REDIRECT_LIMIT = 121; const int CODE_AUTH_FAILED = 122; // Encoding/decoding errors (130-139) const int CODE_ARCHIVING_FAILED = 130; const int CODE_ENCODING_FAILED = 131; const int CODE_DECODING_FAILED = 132; const int CODE_INVALID_FORMAT = 133; // Resource/allocation errors (140-149) const int CODE_ALLOCATION_FAILED = 140; const int CODE_TEMPORARY_FILE_FAILED = 141; // Locking errors (150-159) const int CODE_LOCK_FAILED = 150; const int CODE_LOCK_NOT_SUPPORTED = 151; // Userspace stream errors (160-169) const int CODE_USERSPACE_NOT_IMPLEMENTED = 160; const int CODE_USERSPACE_INVALID_RETURN = 161; const int CODE_USERSPACE_CALL_FAILED = 162; /** * The error code (one of the StreamError::CODE_* constants) */ public int $code; /** * Human-readable error message */ public string $message; /** * Name of the stream wrapper that generated the error */ public string $wrapperName; /** * PHP error severity level (E_WARNING, E_NOTICE, etc.) */ public int $severity; /** * Whether this is a terminating error */ public bool $terminating; /** * Additional context (typically filename or URL), or null */ public ?string $param; /** * Next error in the chain, or null if this is the last error * * The chain is ordered so that the first (most significant) error is returned directly * and subsequent, typically less critical errors follow through the $next chain. This * ordering is intentional: the primary error that caused the operation to fail is the * most useful for error handling and is immediately available without traversal. Follow-up * errors (e.g., cascading failures from sub-operations) are linked for completeness but * are rarely needed in practice. This is the opposite of Exception::$previous because * exceptions chain from effect to cause, while stream errors chain from cause to effect. */ public ?StreamError $next; /** * Check if this is an I/O operation error (codes 10-29) */ public function isIoError(): bool {} /** * Check if this is a file system error (codes 30-69) */ public function isFileSystemError(): bool {} /** * Check if this is a wrapper/protocol error (codes 70-89) */ public function isWrapperError(): bool {} /** * Check if this is a filter error (codes 90-99) */ public function isFilterError(): bool {} /** * Check if this is a cast/conversion error (codes 100-109) */ public function isCastError(): bool {} /** * Check if this is a network/socket error (codes 110-129) */ public function isNetworkError(): bool {} /** * Check if this is an encoding/decoding error (codes 130-139) */ public function isEncodingError(): bool {} /** * Check if this is a resource/allocation error (codes 140-149) */ public function isResourceError(): bool {} /** * Check if this is a locking error (codes 150-159) */ public function isLockError(): bool {} /** * Check if this is a userspace stream error (codes 160-169) */ public function isUserspaceError(): bool {} /** * Check if the error chain contains a specific error code * * Searches through this error and all chained errors for the specified code. * * @param int $code The error code to search for (use StreamError::CODE_* constants) * @return bool True if the code is found in the chain */ public function hasCode(int $code): bool {} /** * Count the number of errors in the chain * * @return int The total number of errors including this one and all chained errors */ public function count(): int {} } ?>
<?php /** * Exception thrown by stream operations in StreamErrorMode::Exception mode * * This exception is thrown for terminating errors when a stream context is configured * with error_mode set to StreamErrorMode::Exception. It provides structured access * to the complete error chain. */ class StreamException extends Exception { /** * Get the structured error object * * Returns the StreamError object containing full error details and any chained errors. * * @return StreamError|null The error object or null if not available */ public function getError(): ?StreamError {} } ?>
<?php /** * Retrieve the most recently stored stream error * * Returns the most recent error that was stored according to the error_store context option. * Each error may chain to additional related errors through the StreamError::$next property. * * @return StreamError|null The most recent error or null if no errors are stored */ function stream_get_last_error(): ?StreamError {} ?>
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.
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:
<?php $error = stream_get_last_error(); while ($error) { echo StreamError::CODE_GENERIC . ": " . $error->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"; } ?>
<?php $context = stream_context_create([ 'stream' => [ '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"; } } } ?>
<?php $context = stream_context_create([ 'stream' => [ '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"; } } } ?>
<?php $errorLog = []; $context = stream_context_create([ 'stream' => [ '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"; } } ?>
<?php $context = stream_context_create([ 'stream' => [ '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"; } } ?>
<?php $context = stream_context_create([ '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"; } } ?>
This RFC adds new functionality and maintains backward compatibility with existing code. The default behavior (StreamErrorMode::Error) preserves all existing error reporting.
Minor changes:
These changes improve correctness and should not negatively impact existing applications.
PHP 8.6
This proposal establishes a foundation for future enhancements:
As per the voting RFC, a yes/no vote with a 2/3 majority is needed for this proposal to be accepted.
The vote started on 2025-XX-XX at XX:XX UTC and ends on 2025-XX-XX at XX:XX UTC.
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.
After the project is implemented, this section will contain: