Engine Exceptions — Error Hierarchy (PHP 7.0)
debt(d5/e1/b3/t7)
Closest to 'specialist tool catches it' (d5). The detection_hints list phpstan and psalm as the tools that catch the pattern `catch.*(Exception` — these are static analysis tools that require deliberate setup, not default linters or compilers. The mistake is invisible at runtime until an Error (e.g. TypeError) is thrown and silently escapes an Exception-only catch block.
Closest to 'one-line patch or single-call swap' (e1). The quick_fix is explicit: replace `catch(Exception)` with `catch(\Throwable)` — a single-line change per catch site. Even for precise handling, swapping to a specific Error subclass is a minimal local edit.
Closest to 'localised tax' (b3). The concept applies across web, cli, and queue-worker contexts, but the fix is localized to each catch block. It doesn't reshape architecture or slow many work streams — it's a per-catch-site concern that affects only developers writing or reviewing error handling code.
Closest to 'serious trap' (t7). The misconception is explicit: developers familiar with PHP 5 or other languages naturally expect `catch (Exception $e)` to catch all thrown things, but in PHP 7 the Error hierarchy is completely separate from Exception. This directly contradicts how exception hierarchies work in most other languages and in PHP 5, making it a serious cross-language/cross-version trap.
TL;DR
Explanation
PHP 7.0 introduced the Error class (distinct from Exception) implementing Throwable. Engine errors that previously caused uncatchable fatals now throw Error subclasses: ParseError (eval syntax errors), TypeError (type declaration violations), ArithmeticError / DivisionByZeroError (math errors), AssertionError (assert() failures), Error (generic engine errors). catch (\Throwable $t) catches everything. catch (\Error $e) catches engine errors only. This unified PHP's error handling — no more register_shutdown_function() to detect fatal errors that were actually TypeError.
Common Misconception
Why It Matters
Common Mistakes
- Catching Exception when TypeError/Error should also be caught.
- Not knowing that each Error subclass can be caught specifically.
- Missing the Throwable interface — it's what unifies Error and Exception.
Code Examples
try {
$result = intdiv(10, 0);
} catch (Exception $e) {
// Never reached! DivisionByZeroError extends Error, not Exception
}
try {
$result = intdiv(10, 0);
} catch (DivisionByZeroError $e) {
$result = 0; // Specific
} catch (\Error $e) {
throw new AppException('Engine error', previous: $e);
} catch (\Throwable $t) {
// Catch-all for both Error and Exception
}