The Throwable hierarchy: Error vs Exception
When something goes wrong, PHP (or your code) throws an object. Everything that can be thrown implements the Throwable interface, and it has exactly two branches. Exception is for problems a program is expected to handle: a missing record, bad input, a network timeout. Error is what PHP itself throws for programming mistakes: calling a method on null, passing the wrong type, dividing by zero. Both can be caught; the split tells you who is at fault.
<?php
function lineage(string $class): string
{
$chain = [$class];
while ($class = get_parent_class($class)) {
$chain[] = $class;
}
return implode(' -> ', $chain);
}
foreach ([
'TypeError', 'ArgumentCountError', 'DivisionByZeroError', 'ValueError',
'InvalidArgumentException', 'RuntimeException', 'JsonException', 'ErrorException',
] as $c) {
$root = is_subclass_of($c, Error::class) ? 'Throwable (Error)' : 'Throwable (Exception)';
echo str_pad(lineage($c), 58), $root, "\n";
}
var_dump(new TypeError() instanceof Throwable);
var_dump(new TypeError() instanceof Exception);TypeError -> Error Throwable (Error)
ArgumentCountError -> TypeError -> Error Throwable (Error)
DivisionByZeroError -> ArithmeticError -> Error Throwable (Error)
ValueError -> Error Throwable (Error)
InvalidArgumentException -> LogicException -> Exception Throwable (Exception)
RuntimeException -> Exception Throwable (Exception)
JsonException -> Exception Throwable (Exception)
ErrorException -> Exception Throwable (Exception)
bool(true)
bool(false)get_parent_class() walks up the chain. A TypeError is a Throwable but not an Exception — the single most important fact in this module.
| Class | Branch | Thrown when |
|---|---|---|
TypeError | Error | An argument, return value or property has the wrong type |
ArgumentCountError | Error | Too few arguments to a function |
ValueError | Error | Right type, impossible value (str_repeat("x", -1)) |
DivisionByZeroError | Error | intdiv($x, 0), $x % 0, $x / 0 |
UnhandledMatchError | Error | No match arm matched |
InvalidArgumentException | Exception (Logic) | Your function got input it refuses — the caller's bug |
DomainException, LengthException, OutOfRangeException | Exception (Logic) | More specific caller mistakes |
RuntimeException | Exception | Something outside the code failed: file, network, database |
JsonException | Exception | json_decode/json_encode with JSON_THROW_ON_ERROR |
PDOException | Exception (Runtime) | A database error with PDO::ERRMODE_EXCEPTION (Module 10) |
Uncaught TypeError even though there is a catch (Exception $e)
<?php
declare(strict_types=1);
function half(int $n): int
{
return intdiv($n, 2);
}
try {
echo half("10");
} catch (Exception $e) {
echo "Something went wrong: ", $e->getMessage();
}Fatal error: Uncaught TypeError: half(): Argument #1 ($n) must be of type int, string given, called in main.php on line 10 and defined in main.php:4
Stack trace:
#0 main.php(10): half('10')
#1 {main}
thrown in main.php on line 4TypeError extends Error, not Exception, so catch (Exception $e) never sees it and the script dies. This surprises everyone who learned PHP 5, where these were fatal errors that could not be caught at all.
Fix the bug (pass an int). If you really need a catch-all — at the very top of a program, to log and show a friendly page — catch Throwable. Do not catch Error deep inside business code; it hides bugs.
<?php
declare(strict_types=1);
function half(int $n): int
{
return intdiv($n, 2);
}
try {
echo half((int) "10"); // 5
} catch (Throwable $e) { // last-resort net catches Error AND Exception
echo "Something went wrong: ", $e->getMessage();
}