Exception hierarchy — Exception vs Error vs Throwable
Concept
PHP's exception hierarchy distinguishes between expected exceptional conditions (\Exception) and programming errors / engine failures (\Error). Both implement the \Throwable interface, which is the base for everything that can be thrown.
\Throwable: The root interface. Catch \Throwable to catch absolutely anything thrown by PHP.
\Exception: Base class for user-space exceptions. Use for business logic errors, domain exceptions, and recoverable problems the application should handle.
\Error: Base class for PHP engine errors that would previously have been fatal. Includes: \TypeError (wrong type passed to typed function), \ValueError (correct type but invalid value, e.g., negative length), \ArithmeticError / \DivisionByZeroError, \ParseError (eval() with syntax error), \ArgumentCountError, \UnhandledMatchError.
Why this hierarchy matters: Library code should typically catch \Exception — it's the application-level contract. Catching \Error means you're trying to recover from a programming mistake, which is usually wrong. Catch \Throwable only at application entry points (HTTP kernel, queue worker loop) to ensure graceful degradation.
The \LogicException vs \RuntimeException distinction: Both extend \Exception. \LogicException (and subclasses: \InvalidArgumentException, \OutOfRangeException, \LengthException, \BadMethodCallException) indicate programming errors — conditions that should be impossible if the code is correct. \RuntimeException (and subclasses: \OutOfBoundsException, \RangeException, \OverflowException) indicate runtime conditions that couldn't be detected statically.
Code Example
<?php
declare(strict_types=1);
// The hierarchy
// \Throwable
// ├── \Error
// │ ├── \TypeError
// │ ├── \ValueError
// │ ├── \ArithmeticError
// │ │ └── \DivisionByZeroError
// │ ├── \ParseError
// │ ├── \ArgumentCountError
// │ └── \UnhandledMatchError
// └── \Exception
// ├── \LogicException
// │ ├── \BadFunctionCallException
// │ │ └── \BadMethodCallException
// │ ├── \DomainException
// │ ├── \InvalidArgumentException
// │ ├── \LengthException
// │ └── \OutOfRangeException
// └── \RuntimeException
// ├── \OutOfBoundsException
// ├── \OverflowException
// ├── \RangeException
// ├── \UnderflowException
// └── \UnexpectedValueException
// TypeError — thrown for wrong types
function strictAdd(int $a, int $b): int { return $a + $b; }
try {
strictAdd('hello', 'world'); // TypeError with strict_types=1
} catch (\TypeError $e) {
echo "TypeError: " . $e->getMessage() . "\n";
}
// ValueError — valid type, invalid value
try {
$arr = array_chunk([], -1); // negative chunk size
} catch (\ValueError $e) {
echo "ValueError: " . $e->getMessage() . "\n";
}
// UnhandledMatchError
try {
$result = match(99) { 1 => 'one', 2 => 'two' }; // no match, no default
} catch (\UnhandledMatchError $e) {
echo "No match case for value 99\n";
}
// DivisionByZeroError
try {
$result = intdiv(5, 0);
} catch (\DivisionByZeroError $e) {
echo "Division by zero\n";
}
// Catching all thrown values at the top level
function applicationEntryPoint(): void
{
try {
runApplication();
} catch (\Exception $e) {
log_exception($e);
display_error_page(500);
} catch (\Error $e) {
// Programming error — more serious
log_fatal($e);
display_error_page(500);
}
}