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.
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
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:
Throwable
, the check
expression performs a throw
(effectively yielding never
)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.
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.)
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.
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.
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.)
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.
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.
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.
This feature has no impact on existing exception behavior.
For this proposal to be accepted, a 2/3 majority is required.
No patch has been written for this RFC.