来源:https://trowski.com/2015/06/24/throwable-exceptions-and-errors-in-php7/
Handling fatal errors in the past has been next to impossible in PHP. A fatal error would not invoke the error handler set by set_error_handler()
and would simply halt script execution.
In PHP 7, an exception will be thrown when a fatal and recoverable error (E_ERROR
and E_RECOVERABLE_ERROR
) occurs, rather than halting script execution. Fatal errors still exist for certain conditions, such as running out of memory, and still behave as before by immediately halting script execution. An uncaught exception will also continue to be a fatal error in PHP 7. This means if an exception thrown from an error that was fatal in PHP 5.x goes uncaught, it will still be a fatal error in PHP 7.
Note that other types of errors such as warnings and notices remain unchanged in PHP 7. Only fatal and recoverable errors throw exceptions.
Exceptions thrown from fatal and recoverable errors do not extend Exception
. This separation was made to prevent existing PHP 5.x code from catching exceptions thrown from errors that used to halt script execution. Exceptions thrown from fatal and recoverable errors are instances of a new and separate exception class: Error
. Like any other exception, Error
may be caught and handled and will allow any finally
blocks to be executed.
Prior to PHP 7 alpha-2, the exception hierarchy in PHP 7 was different. Fatal and recoverable errors threw instances ofEngineException
, which did not inherit fromException
. BothException
andEngineException
inherited fromBaseException
. The hierarchy was revised with the RFC I authored, Throwable Interface. I felt switching toThrowable
andError
was important to avoid confusion from classes using the suffix Exception that did not extendException
, as well as being more concise and appealing names.
Throwable
To unite the two exception branches, Exception
and Error
both implement a new interface, Throwable
.
The new exception hierarchy in PHP 7 is as follows:
interface Throwable
|- Exception implements Throwable
|- ...
|- Error implements Throwable
|- TypeError extends Error
|- ParseError extends Error
|- ArithmeticError extends Error
|- DivisionByZeroError extends ArithmeticError
|- AssertionError extends Error
If Throwable
was defined in PHP 7 code, it would look like the code below.
interface Throwable
{
public function getMessage(): string;
public function getCode(): int;
public function getFile(): string;
public function getLine(): int;
public function getTrace(): array;
public function getTraceAsString(): string;
public function getPrevious(): Throwable;
public function __toString(): string;
}
This interface should look familiar. Throwable
specifies methods nearly identical to those of Exception
. The only difference is that Throwable::getPrevious()
can return any instance of Throwable
instead of just an Exception
. The constructors of Exception
and Error
accept any instance of Throwable
as the previous exception.
Throwable
may be used in try/catch
blocks to catch both Exception
and Error
objects (or any possible future exception types). Remember that it is better practice to catch more specific exception classes and handle each accordingly. However, some situations warrant catching any exception (such as for logging or framework error handling). In PHP 7, these catch-all blocks should catch Throwable
instead of Exception
.
try {
// Code that may throw an Exception or Error.
} catch (Throwable $t) {
// Handle exception
}
User-defined classes cannot implement Throwable
. This choice was made in part for predictability and consistency: only instances of Exception
or Error
may be thrown. Additionally, exceptions carry information about where the object was created in the stack trace. A user-defined object does not automatically have parameters to store this information.
Throwable
can be extended to create package-specific interfaces or add additional methods. An interface extending Throwable
can only be implemented by a class extending either Exception
or Error
.
interface MyPackageThrowable extends Throwable {}
class MyPackageException extends Exception implements MyPackageThrowable {}
throw new MyPackageException();
Error
Virtually all errors in PHP 5.x that were fatal errors or recoverable fatal errors now throw instances of Error
in PHP 7. Like any other exception, Error
objects can be caught using a try/catch
block.
$var = 1;
try {
$var->method(); // Throws an Error object in PHP 7.
} catch (Error $e) {
// Handle error
}
Usually an instance of the base Error
class is thrown from previously fatal errors, but some errors will throw a more specific subclass of Error
: TypeError
, ParseError
, and AssertionError
.
TypeError
A TypeError
instance is thrown when a function argument or return value does not match a type declaration.
function add(int $left, int $right)
{
return $left + $right;
}
try {
$value = add('left', 'right');
} catch (TypeError $e) {
echo $e->getMessage(), "\n";
}
Argument 1 passed to add() must be of the type integer, string given
ParseError
A ParseError
is thrown when an included/required file or eval()
'd code contains a syntax error.
try {
require 'file-with-parse-error.php';
} catch (ParseError $e) {
echo $e->getMessage(), "\n";
}
ArithmeticError
An ArithmeticError
is thrown in two situations: bit shifting by a negative number or calling intdiv()
with a numerator of PHP_INT_MIN
and denominator of -1
(the expression using the division (/
) operator, PHP_INT_MIN / -1
, will return a float).
try {
$value = 1 << -1;
} catch (ArithmeticError $e) {
echo $e->getMessage(), "\n";
}
DivisionByZeroError
A DivisionByZeroError
is thrown from intdiv()
when the denominator is zero or when zero is used as the denominator with the modulo (%
) operator. Note that using zero with the division (/
) operator only issues a warning and evaluates to NaN
if the numerator is zero or Inf
for any non-zero numerator.
try {
$value = 1 % 0;
} catch (DivisionByZeroError $e) {
echo $e->getMessage(), "\n";
}
AssertionError
When the condition set by assert()
is not met, an AssertionError
will be thrown.
ini_set('zend.assertions', 1);
ini_set('assert.exception', 1);
$test = 1;
assert($test === 0);
Fatal error: Uncaught AssertionError: assert($test === 0)
assert()
is only executed and will only throw an AssertionError
if assertions are enabled and set to throw exceptions with ini settings zend.assertions = 1
and assert.exception = 1
.
Using Error in Your Code
Users are able to create Error
as well as extend Error
to create your own hierarchy of Error
classes. This poses an important question: what situations should throw an instance of a class extending Exception
and what situations should throw an instance of a class extending Error
?
Error
should be used to represent coding issues that require the attention of a programmer. Error
objects thrown from the PHP engine fall into this category, as they generally result from coding errors such as providing a parameter of the wrong type to a function or a parse error in a file. Exception
should be used for conditions that can be safely handled at runtime where another action can be taken and execution can continue.
Since Error
objects should not be handled at runtime, catching Error
objects should be uncommon. In general, Error
objects should only be caught for logging, performing any necessary cleanup, and display an error message to the user.
Writing Code to Support PHP 5.x and 7 Exceptions
To catch any exception in PHP 5.x and 7 with the same code, multiple catch blocks can be used, catching Throwable
first, then Exception
. Once PHP 5.x support is no longer needed, the block catching Exception
can be removed.
try {
// Code that may throw an Exception or Error.
} catch (Throwable $t) {
// Executed only in PHP 7, will not match in PHP 5.x
} catch (Exception $e) {
// Executed only in PHP 5.x, will not be reached in PHP 7
}
Unfortunately, type declarations on functions that handle exceptions are not as easy to fix. If Exception
is used as a type declaration on a function parameter, the type declaration will need to be removed if the function could be called with an instance of Error
. When PHP 5.x support is not required, the type declaration can be restored as Throwable
.