rfc:stream_errors

PHP RFC: Stream Error Handling Improvements

  • Version: 2.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 with structured error objects
  • Semantic error codes - well-defined enum-based codes 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_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.

API

StreamErrorMode Enum

<?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;
}
 
?>

StreamErrorStore Enum

<?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;
}
 
?>

StreamErrorCode Enum

<?php
 
/**
 * Semantic error codes for stream operations
 */
enum StreamErrorCode: int
{
    // General errors
    case None = 0;
    case Generic = 1;
 
    // I/O operation errors (10-29)
    case ReadFailed = 10;
    case WriteFailed = 11;
    case SeekFailed = 12;
    case SeekNotSupported = 13;
    case FlushFailed = 14;
    case TruncateFailed = 15;
    case ConnectFailed = 16;
    case BindFailed = 17;
    case ListenFailed = 18;
    case NotWritable = 19;
    case NotReadable = 20;
 
    // File system operation errors (30-69)
    case Disabled = 30;
    case NotFound = 31;
    case PermissionDenied = 32;
    case AlreadyExists = 33;
    case InvalidPath = 34;
    case PathTooLong = 35;
    case OpenFailed = 36;
    case CreateFailed = 37;
    case DupFailed = 38;
    case UnlinkFailed = 39;
    case RenameFailed = 40;
    case MkdirFailed = 41;
    case RmdirFailed = 42;
    case StatFailed = 43;
    case MetaFailed = 44;
    case ChmodFailed = 45;
    case ChownFailed = 46;
    case CopyFailed = 47;
    case TouchFailed = 48;
    case InvalidMode = 49;
    case InvalidMeta = 50;
    case ModeNotSupported = 51;
    case Readonly = 52;
    case RecursionDetected = 53;
 
    // Wrapper/protocol errors (70-89)
    case NotImplemented = 70;
    case NoOpener = 71;
    case PersistentNotSupported = 72;
    case WrapperNotFound = 73;
    case WrapperDisabled = 74;
    case ProtocolUnsupported = 75;
    case WrapperRegistrationFailed = 76;
    case WrapperUnregistrationFailed = 77;
    case WrapperRestorationFailed = 78;
 
    // Filter errors (90-99)
    case FilterNotFound = 90;
    case FilterFailed = 91;
 
    // Cast/conversion errors (100-109)
    case CastFailed = 100;
    case CastNotSupported = 101;
    case MakeSeekableFailed = 102;
    case BufferedDataLost = 103;
 
    // Network/socket errors (110-129)
    case NetworkSendFailed = 110;
    case NetworkRecvFailed = 111;
    case SslNotSupported = 112;
    case ResumptionFailed = 113;
    case SocketPathTooLong = 114;
    case OobNotSupported = 115;
    case ProtocolError = 116;
    case InvalidUrl = 117;
    case InvalidResponse = 118;
    case InvalidHeader = 119;
    case InvalidParam = 120;
    case RedirectLimit = 121;
    case AuthFailed = 122;
 
    // Encoding/decoding errors (130-139)
    case ArchivingFailed = 130;
    case EncodingFailed = 131;
    case DecodingFailed = 132;
    case InvalidFormat = 133;
 
    // Resource/allocation errors (140-149)
    case AllocationFailed = 140;
    case TemporaryFileFailed = 141;
 
    // Locking errors (150-159)
    case LockFailed = 150;
    case LockNotSupported = 151;
 
    // Userspace stream errors (160-169)
    case UserspaceNotImplemented = 160;
    case UserspaceInvalidReturn = 161;
    case UserspaceCallFailed = 162;
 
    /**
     * 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 {}
}
 
?>

StreamError Class

<?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
{
    /**
     * The error code
     */
    public StreamErrorCode $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
     */
    public ?StreamError $next;
 
    /**
     * Check if the error chain contains a specific error code
     * 
     * Searches through this error and all chained errors for the specified code.
     * 
     * @param StreamErrorCode $code The error code to search for
     * @return bool True if the code is found in the chain
     */
    public function hasCode(StreamErrorCode $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 {}
}
 
?>

StreamException Class

<?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 {}
}
 
?>

stream_get_last_error Function

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

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 $error->code->name . ": " . $error->message . "\n";
    $error = $error->next;
}
 
// Or count them
$totalErrors = $error->count();
 
// Or search for specific codes
if ($error->hasCode(StreamErrorCode::NotImplemented)) {
    echo "Stream does not implement required functionality\n";
}
 
?>

Usage Examples

Exception Mode

<?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->name . "\n";
        if ($error->param) {
            echo "File: " . $error->param . "\n";
        }
    }
}
 
?>

Silent Mode with Error Storage

<?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->value}: {$error->message}\n";
 
        if ($error->code === StreamErrorCode::NotFound) {
            echo "Resource not found, using fallback...\n";
        }
 
        // Check error category
        if ($error->code->isNetworkError()) {
            echo "Network-related error occurred\n";
        }
    }
}
 
?>

Custom Error Handler

<?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->name,
                '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

<?php
 
$context = stream_context_create([
    'stream' => [
        'error_mode' => StreamErrorMode::Silent,
        'error_store' => StreamErrorStore::All,
    ]
]);
 
// This may generate multiple chained errors
$read = [$userStream];
stream_select($read, $write, $except, 0, $context);
 
$error = stream_get_last_error();
 
if ($error) {
    // Process all errors in the chain
    $current = $error;
    while ($current) {
        echo "- {$current->code->name}: {$current->message}\n";
        $current = $current->next;
    }
 
    // Or use helper methods
    echo "Total errors: " . $error->count() . "\n";
 
    if ($error->hasCode(StreamErrorCode::CastNotSupported)) {
        echo "Stream cannot be used with select()\n";
    }
}
 
?>

Setting Default Context

<?php
 
stream_context_set_default([
    'stream' => [
        'error_mode' => StreamErrorMode::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";
 
    $error = $e->getError();
    if ($error && $error->code->isFileSystemError()) {
        echo "This is a filesystem-related error\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

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.

Should the Stream Error Handling Improvements be added to PHP core?
Real name Yes No Abstain
Final result: 0 0 0
This poll has been closed.

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:

  1. the version(s) it was merged to
  2. a link to the git commit(s)

References

Changelog

  • 2.0 - Major API redesign with enums, error chaining, and simplified retrieval
  • 1.0 - Initial version
rfc/stream_errors.txt · Last modified: by bukka