0

Exception hierarchy — Exception vs Error vs Throwable

Intermediate5 min read·php-10-004
interview

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
<?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);
    }
}