rfc:stream_errors

PHP RFC: Stream Error Handling Improvements

  • Version: 2.3
  • 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 - a dedicated enum for different error conditions
  • Error grouping - track multiple related errors from a single operation as an array

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, a ValueError exception is thrown.

error_store

Controls which errors are stored for later retrieval via stream_last_errors(). 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_last_errors() which returns an array of StreamError objects from the most recent stored operation.

error_handler

Optional callback to receive error notifications. The callback receives an array of StreamError objects:

function(array $errors): 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

StreamErrorCode Enum

<?php
 
/**
 * Error codes for stream operations
 */
enum StreamErrorCode
{
    case None;
    case Generic;
    case ReadFailed;
    case WriteFailed;
    case SeekFailed;
    case SeekNotSupported;
    case FlushFailed;
    case TruncateFailed;
    case ConnectFailed;
    case BindFailed;
    case ListenFailed;
    case NotWritable;
    case NotReadable;
    case Disabled;
    case NotFound;
    case PermissionDenied;
    case AlreadyExists;
    case InvalidPath;
    case PathTooLong;
    case OpenFailed;
    case CreateFailed;
    case DupFailed;
    case UnlinkFailed;
    case RenameFailed;
    case MkdirFailed;
    case RmdirFailed;
    case StatFailed;
    case MetaFailed;
    case ChmodFailed;
    case ChownFailed;
    case CopyFailed;
    case TouchFailed;
    case InvalidMode;
    case InvalidMeta;
    case ModeNotSupported;
    case Readonly;
    case RecursionDetected;
    case NotImplemented;
    case NoOpener;
    case PersistentNotSupported;
    case WrapperNotFound;
    case WrapperDisabled;
    case ProtocolUnsupported;
    case WrapperRegistrationFailed;
    case WrapperUnregistrationFailed;
    case WrapperRestorationFailed;
    case FilterNotFound;
    case FilterFailed;
    case CastFailed;
    case CastNotSupported;
    case MakeSeekableFailed;
    case BufferedDataLost;
    case NetworkSendFailed;
    case NetworkRecvFailed;
    case SslNotSupported;
    case ResumptionFailed;
    case SocketPathTooLong;
    case OobNotSupported;
    case ProtocolError;
    case InvalidUrl;
    case InvalidResponse;
    case InvalidHeader;
    case InvalidParam;
    case RedirectLimit;
    case AuthFailed;
    case ArchivingFailed;
    case EncodingFailed;
    case DecodingFailed;
    case InvalidFormat;
    case AllocationFailed;
    case TemporaryFileFailed;
    case LockFailed;
    case LockNotSupported;
    case UserspaceNotImplemented;
    case UserspaceInvalidReturn;
    case UserspaceCallFailed;
}
 
?>

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

StreamError Class

<?php
 
/**
 * Represents a single stream error with full context
 *
 * StreamError objects are immutable value objects. When a stream operation produces
 * multiple errors, they are returned as an array of StreamError objects.
 */
final readonly class StreamError
{
    /**
     * The error code identifying the type of error
     */
    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;
}
 
?>

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 list of errors from the operation.
 */
class StreamException extends Exception
{
    /**
     * Get the structured error objects
     * 
     * Returns an array of StreamError objects containing full error details from
     * the operation that caused the exception.
     * 
     * @return StreamError[] The error objects
     */
    public function getErrors(): array {}
}
 
?>

stream_last_errors Function

<?php
 
/**
 * Retrieve the errors from the most recently stored stream operation
 * 
 * Returns an array of StreamError objects that were stored according to the
 * error_store context option. The array is ordered with the primary (most
 * significant) error first.
 * 
 * Each new stream operation that produces storable errors automatically replaces
 * the previous result, so there is no need to clear errors between operations.
 * 
 * @return StreamError[] Array of error objects, empty if no errors are stored
 */
function stream_last_errors(): array {}
 
?>

stream_clear_errors Function

<?php
 
/**
 * Clear all stored stream errors
 * 
 * Removes all errors that have been accumulated from previous stream operations.
 * This is not needed for isolating errors between operations, as stream_last_errors()
 * always returns only the most recent operation's errors. This function is useful when
 * stored errors are no longer needed and should be explicitly discarded.
 */
function stream_clear_errors(): void {}
 
?>

Error Grouping

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 returned together as an array of StreamError objects.

The array is ordered from most significant to least significant: the primary error that caused the operation to fail is the first element, and follow-up errors from sub-operations appear in subsequent elements.

For example, a failed stream_select() on a userspace stream might generate:

  1. A terminating error that stream_cast() is not implemented
  2. A terminating error that the stream type cannot be represented as a descriptor

Both errors are available in the array and can be processed using standard PHP array functions:

<?php
 
$errors = stream_last_errors();
 
// Iterate all errors
foreach ($errors as $error) {
    echo $error->code->name . ": " . $error->message . "\n";
}
 
// Count them
$totalErrors = count($errors);
 
// Get the primary error
$primary = array_first($errors);
 
// Search for specific codes
if (array_any($errors, fn($e) => $e->code === StreamErrorCode::NotImplemented)) {
    echo "Stream does not implement required functionality\n";
}
 
// Find a specific error
$notImpl = array_find($errors, fn($e) => $e->code === StreamErrorCode::NotImplemented);
if ($notImpl) {
    echo "Not implemented: " . $notImpl->message . "\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";
 
    $errors = $e->getErrors();
    $first = array_first($errors);
    if ($first) {
        echo "Wrapper: " . $first->wrapperName . "\n";
        echo "Error Code: " . $first->code->name . "\n";
        if ($first->param) {
            echo "File: " . $first->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) {
    $errors = stream_last_errors();
    $first = array_first($errors);
 
    if ($first) {
        echo "Error: {$first->code->name}: {$first->message}\n";
 
        if ($first->code === StreamErrorCode::NotFound) {
            echo "Resource not found, using fallback...\n";
        }
    }
}
 
?>

Custom Error Handler

<?php
 
$errorLog = [];
 
$context = stream_context_create([
    'stream' => [
        'error_mode' => StreamErrorMode::Silent,
        'error_handler' => function(array $errors) use (&$errorLog) {
            foreach ($errors as $error) {
                $errorLog[] = [
                    'timestamp' => time(),
                    'wrapper' => $error->wrapperName,
                    'code' => $error->code->name,
                    'message' => $error->message,
                    'param' => $error->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";
}
 
?>

Processing Errors from stream_select

<?php
 
$context = stream_context_create([
    'stream' => [
        'error_mode' => StreamErrorMode::Silent,
        'error_store' => StreamErrorStore::All,
    ]
]);
 
// This may generate multiple errors
$read = [$userStream];
$write = NULL;
$except = NULL;
stream_select($read, $write, $except, 0, 0, $context);
 
$errors = stream_last_errors();
 
if ($errors) {
    foreach ($errors as $error) {
        echo "- {$error->code->name}: {$error->message}\n";
    }
 
    echo "Total errors: " . count($errors) . "\n";
 
    if (array_any($errors, fn($e) => $e->code === StreamErrorCode::CastNotSupported)) {
        echo "Stream cannot be used with select()\n";
    }
}
 
?>

Using Context with stream_copy_to_stream

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

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
  • Error code categories - Convenience methods or interfaces for grouping related 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.

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 2026-XX-XX at XX:XX UTC and ends on 2026-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.3 - Remove error code categories and is*Error() methods from StreamErrorCode enum, defer to future scope
  • 2.2 - Replace StreamError class constants with StreamErrorCode enum, replace linked-list error chaining with arrays and related functions updates, added stream_clear_errors()
  • 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
rfc/stream_errors.txt · Last modified: by bukka