Exception Hierarchy (Throwable, Error, Exception)
debt(d5/e1/b3/t7)
Closest to 'specialist tool catches it' (d5). The detection_hints list phpstan and psalm, both static analysis tools, as the tools that catch the `catch (Exception` pattern. This is not a default linter catch (d3) nor a compiler error (d1); it requires running a specialist static analyser configured to flag incomplete catch hierarchies.
Closest to 'one-line patch or single-call swap' (e1). The quick_fix states: replace `catch (Exception $e)` with `catch (\Throwable $t)` — a literal single-line substitution. Even catching specific subclasses is a minimal, local change at each catch site.
Closest to 'localised tax' (b3). The applies_to covers web, cli, and queue-worker contexts (broad PHP usage), but the impact is scoped to error-handling code only. It doesn't shape the overall architecture; it is a persistent awareness tax at every catch block rather than a system-wide gravitational pull.
Closest to 'serious trap' (t7). The misconception field states explicitly: developers believe `catch (Exception $e)` catches everything, but it silently misses all Error subclasses (TypeError, ParseError, etc.) which only implement Throwable. This directly contradicts intuition from other languages (Java, C#) where a top-level Exception catch is truly exhaustive, making it a cross-ecosystem behavioural contradiction rather than just a documented gotcha.
TL;DR
Explanation
PHP 7 introduced Throwable as the root interface. Exception (user exceptions) and Error (engine errors like TypeError, ParseError, ArithmeticError) both implement it. Before PHP 7, fatal errors like calling undefined functions were uncatchable. Now: catch (\Throwable $t) catches everything. catch (\Error $e) catches engine errors. catch (\Exception $e) catches application exceptions. Key subclasses: TypeError (wrong types), ValueError (invalid argument values), ArithmeticError / DivisionByZeroError, ParseError, Error.
Common Misconception
Why It Matters
Common Mistakes
- Catching Exception when Error subclasses are possible — use Throwable.
- Not distinguishing between recoverable errors (TypeError from user input) and programming errors (wrong arg type in internal code).
- Swallowing Throwable in catch blocks without logging.
Code Examples
try {
$result = intdiv($a, 0);
} catch (Exception $e) {
// Misses DivisionByZeroError — it's an Error, not Exception
echo "caught";
}
try {
$result = intdiv($a, 0);
} catch (DivisionByZeroError $e) {
$result = 0; // specific handling
} catch (\TypeError $e) {
throw new InvalidArgumentException('Numeric values required', 0, $e);
} catch (\Throwable $t) {
// Last resort — log everything
logger()->critical('Unhandled', ['exception' => $t]);
throw $t;
}