====== PHP RFC: Check Operator ====== * Version: 0.1 * Date: 2024-12-16 * Author: Rasmus Schultz, rasmus@mindplay.dk * Status: Draft * First Published at: https://wiki.php.net/rfc/check-operator ===== Introduction ===== PHP is traditionally reliant on exception-based error handling. In recent years, the PHP type system has evolved and improved considerably, while error handling has not. Many modern programming languages, frameworks and libraries, are moving towards functional programming paradigms, and (especially) towards more explicit error handling models that treat errors as first-class values, enabling more intentional and granular error handling. This RFC proposes the introduction of a ''%%check%%'' operator: a language-level mechanism to conditionally ''%%throw%%'' when the returned value from a function call yields a ''%%Throwable%%'' type. ===== Overview ===== The ''%%check%%'' operator is designed to be used in conjunction with return type unions - the following is a small, motivating example demonstrating its use: function divide(int $a, int $b): int|DivisionByZeroError { if ($b == 0) { return new DivisionByZeroError("Cannot divide by zero"); } return $a / $b; } // Normal usage - does not throw exceptions: $result = divide(10, 0); if (is_int($result)) { // do something } else { // handle error } // Using the `check` operator to declaratively throw: $result = check divide(10, 0); // Throws if a Throwable is returned ===== Proposal ===== The ''%%check%%'' operator is a right-associative operator, which applies to function and method calls, e.g.: $a = check myFunction(); $b = check $myObject->myMethod(); The ''%%check%%'' operator checks the return value from the function call, and evaluates the result, such that: - If the return value is a ''%%Throwable%%'', the ''%%check%%'' expression performs a ''%%throw%%'' (effectively yielding ''%%never%%'') - If the return value is //not// a ''%%Throwable%%'', the ''%%check%%'' expression evaluates to the return value of the function. Traditionally, exceptions of any type can be thrown anywhere in the call stack, making it difficult to reason about where, how, and what type of errors might occur. The ''%%check%%'' operator encourages functions with better locality of behavior, and precise type declarations which can be checked (by existing type checking features) at run-time. The ''%%check%%'' operator offers an alternative to throwing exceptions in functions, enabling the call site to explicitly define, or declaratively control, the error handling behavior. In addition, the ''%%check%%'' operator provides better support for static analysis in tools and IDEs, by enabling the removal of any ''%%Throwable%%'' types (from a return type union) during static analysis, improving type inference of local variables. ==== Stack Trace Behavior ==== The call stack of an implied ''%%throw%%'', when generated by the ''%%check%%'' operator, begins at the call site where the ''%%check%%'' operator was applied. (Note that, in PHP, exceptions are not populated unless they are thrown - the stack trace and line number of an exception produced by a function call //without// a ''%%check%%'' operator do not get populated.) ==== Impact on Existing Code ==== Many existing codebases have static factory methods for creating errors - these can be directly leveraged: class ValidationError extends RuntimeException { public static function invalidInput(string $field): self { return new self("Invalid input for field: $field"); } } function validateUser(array $data): User|ValidationError { if (empty($data['name'])) { return ValidationError::invalidInput('name'); } return new User($data); } // Usage in a controller: $result = validateUser($data); return match(true) { $result instanceof User => new JsonResponse(200, ["user_id" => $result->getUserID()]), $result instanceof ValidationError => new JsonResponse(500, ["error" => $result->getMessage()]), default => check $result // unhandled error type, generates an exception } // Usage in a command, service, background process etc.: $user = check validateUser($data); // Throws if ValidationError Note that, while the ''%%check%%'' operator enables the use of return type unions for exception-free error reporting, it has impact on existing code that throws exceptions - this feature does not alter the behavior of exceptions, but instead motivates the use of more explicit error unions in return types, which itself motivates the use of local and immediate error handling. ==== Wrapping Unsafe Code ==== The ''%%check%%'' operator has no impact on existing code that throws, but can be used to with existing ''%%try-catch%%'' error handling - for example, here we wrap an unsafe function call in a type-safe function: function safeOperation(): int|ProcessException { try { return riskyOperation(); } catch (ProcessException $e) { return $e; // return instead of throwing } } $result = check safeOperation(); This approach provides local control of the error handling behavior. ==== Performance Implications ==== The check operator itself is expected to have a small type-checking overhead - the feature should have no negative performance impact on anything else. (In some cases, the use of ''%%check%%'' might provide some minor performance improvement, by avoiding unnecessary capture or propagation of stack frames, in cases where expected error conditions can be handled locally - however, this is not a primary motivation for this feature.) ==== Impact on the error suppression operator ==== The ''@'' error suppression operator, when applied to a function call in a ''%%check%%'' expression (as in e.g. ''%%$user = check @loadUser(1)%%'') may suppress warnings or notices, altering the code path within that function. This has no bearing on the behavior of the ''%%check%%'' operator itself, which only handles ''%%Throwable%%'' values - if the function call with error suppression evaluates to ''null'', that value will pass through as the result of the expression. ==== No impact on error handling ==== The ''%%check%%'' operator is means of generating exceptions, not a means of handling them. If the ''%%check%%'' operator is applied to a function that throws an exception, the function call never returns, and so the ''%%check%%'' operator does not interfere with normal error propagation. Besides potentially generating an implicit ''%%throw%%'', the ''%%check%%'' operator has no influence on the behavior of ''%%try/catch%%'' blocks - exceptions generated by the ''%%check%%'' operator behave exactly like those generated by a ''%%throw%%'' statement. Use of the ''%%check%%'' operator does not interfere with the ''%%set_error_handler%%'' or ''%%set_exception_handler%%''. The ''%%check%%'' operator does not interfere with type-hint enforcement - a function that returns something invalid (according to its return type) will trigger an error, as usual. ===== Backward Incompatible Changes ===== This feature is fully backwards compatibility with existing code, and can be incrementally adopted, offering a path towards more robust and type-safe error handling in many scenarios, without wholesale rewrites. ===== Unaffected PHP Functionality ===== This feature has no impact on existing exception behavior. ===== Proposed Voting Choices ===== For this proposal to be accepted, a 2/3 majority is required. ===== Patches and Tests ===== No patch has been written for this RFC.