PHP RFC: Throwable Interface
- Version: 0.1.4
- Date: 2015-05-22
- Author: Aaron Piotrowski aaron@icicle.io
- Status: Accepted
- First Published at: http://wiki.php.net/rfc/throwable-interface
Introduction
PHP 7 has introduced exceptions as a replacement for fatal or recoverable fatal errors (see the Exceptions in the Engine RFC). These exceptions do not extend Exception
, but instead extend a new class BaseException
and are named EngineException
, TypeException
, and ParseException
. These classes have a naming scheme that is unintuitive and will lead to confusion, especially for newer users.
Since the decision was made to separate the exception branches, then that separation should be clear instead of obfuscated by similar class names.
Example:
function add(int $left, int $right) { return $left + $right; } try { echo add('left', 'right'); } catch (Exception $e) { // Handle or log exception. }
The code above will not catch the TypeException
thrown due to the mis-matched type-hint, resulting in the following message to the user:
Fatal error: Uncaught TypeException: Argument 1 passed to add() must be of the type integer, string given
The reason an object named TypeException
would not be caught by catch (Exception $e)
is not obvious. The Exception
suffix implies that TypeException
extends Exception
. If the name of the thrown class was TypeError
it would be much clearer that the class does not extend Exception
, but rather is part of a different class hierarchy that must be caught separately.
To catch the TypeException
, the user must write code like this:
function add(int $left, int $right) { return $left + $right; } try { echo add('left', 'right'); } catch (Exception $e) { // Handle exception } catch (TypeException $e) { // Appears to descend from Exception // Log error and end gracefully }
Proposal
This RFC proposes a change to the exception hierarchy for PHP 7. This proposal is based on the Throwable pull request created by Sebastian Bergmann and has been fully implemented in the Throwable Interface pull request.
interface Throwable
Exception implements Throwable
Error implements Throwable
(ReplacesEngineException
)TypeError extends Error
ParseError extends Error
BaseException
will be removed.
Note that ParseError
extends Error
, whereas ParseException
extended BaseException
. This allows users to handle everything that used to be an error in a single catch block, while still allowing the flexibility of handling both separately if desired.
Only objects that implement the Throwable
interface can be thrown. The proposed patch does not allow userland classes to implement the Throwable
interface. Instead all classes declared in userland must extend one of the existing exception classes.
The Throwable
interface specifies the following methods:
getMessage()
getCode()
getFile()
getLine()
getTrace()
getTraceAsString()
__toString()
While both Exception
and Error
are implemented using the same code in the interpreter, the Throwable
interface does not preclude future classes from implementing the interface differently or from the implementation of Exception
and Error
to be different in the future.
catch (Error $e)
and catch (Throwable $e)
may be used to catch respectively Error
objects or any Throwable
(current or future) object. Users should generally be discouraged from catching Error
objects except for logging or cleanup purposes as Error
objects represent coding problems that should be fixed rather than runtime conditions that may be handled.
After this proposed change, it would be clearer in the example above that another catch block is needed if the user wishes to log errors and end the script gracefully.
function add(int $left, int $right) { return $left + $right; } try { echo add('left', 'right'); } catch (Exception $e) { // Handle exception } catch (Error $e) { // Clearly a different type of object // Log error and end gracefully }
Error Name Choice
The name Error
was chosen to correspond with PHP's other errors. Non-fatal errors detected by PHP will continue to trigger warnings and notices, while fatal errors are thrown as Error
exceptions.
Conceptually both of these conditions are error conditions detected by PHP. The only difference is that for some errors the execution of the script can continue from where the error occurred; for others it is not possible for execution to continue from the place where the error occurred, and so instead an exception must be thrown.
While this name may also cause some confusion for users, other name choices such as Failure
do not seem appropriate. It is likely that users would use the term 'Uncaught Error' when searching, minimizing overlap with with non-fatal error messages.
AssertionException
If this RFC is accepted, AssertionException
should instead extend Error
and be renamed to AssertionError
as suggested in the Expectations RFC.
Proposed PHP Version
PHP 7
Backwards Compatibility
Throwable
, Error
, TypeError
, and ParseError
will be built-in interfaces/classes and so it will no longer be possible for users to create classes with those exact names. It will still be possible for those names to be used within a non-global namespace.
Patch
A patch for this RFC is available at https://github.com/php/php-src/pull/1284.
Voting
A majority of 50%+1 is required to approve this RFC.
Please remember that this vote is not about creating separate exception branches, as that decision was made in the Exceptions in the Engine RFC. This vote is about having short, concise, more intuitive names that clarify the separation rather than obfuscating it. Throwable
and Error
make stacked catch blocks cleaner with more obvious behavior. Voting no means keeping BaseException
, EngineException
, etc. that do not extend Exception
, but are named as though they do.
Voting opened June 10th, 2015 and remained open until June 17th, 2015.
References
- Throwable Interface Pull Request by Aaron Piotrowski
- Throwable Pull Request by Sebastian Bergmann