Exception Handling (try/catch/finally)
debt(d5/e3/b5/t7)
Closest to 'specialist tool catches it' (d5). The detection_hints list phpstan, psalm, and phpcs as the tools that catch the common misuse patterns (empty catch blocks, catch-all swallowing). These are static analysis / specialist tools, not the compiler or a default linter, so d5 is the right anchor. The misuse can persist undetected in production if these tools are not run.
Closest to 'simple parameterised fix' (e3). The quick_fix is to replace broad catch clauses with specific exception types and restructure catch hierarchies — a pattern replacement within one component or file. It is more than a one-line patch (requires identifying all catch sites and deciding what to handle vs. propagate) but does not require cross-file or architectural rework in typical cases.
Closest to 'persistent productivity tax' (b5). Exception handling applies across web, cli, and queue-worker contexts (all three listed in applies_to), meaning every future maintainer working in any context must reason about the exception hierarchy and propagation strategy. A poorly structured hierarchy (or catch-all swallowing) imposes ongoing cognitive overhead on every error-handling decision, touching many work streams without necessarily defining the entire system's shape.
Closest to 'serious trap' (t7). The misconception field states explicitly that catch (Exception $e) is widely believed to catch all PHP errors, but PHP 7+ separates Error from Exception under Throwable — TypeError, ArithmeticError, and ParseError all escape. This contradicts how exception catching works in many other languages (Java, Python, C#) where the base exception class does catch everything, making it a cross-language expectation violation that competent developers routinely fall into.
Also Known As
TL;DR
Explanation
PHP exceptions should be used for exceptional, unrecoverable conditions — not for flow control. Catch specific exception types rather than the base Exception or Throwable, use finally blocks for guaranteed resource cleanup (closing files, releasing locks), and never swallow exceptions silently in an empty catch block. PHP 8.0 introduced non-capturing catches (catch (ExceptionType)) when the exception object itself isn't needed. Custom exception hierarchies help callers handle different error categories appropriately.
Diagram
flowchart TD
TRY[try block executes] --> THROWS{Exception thrown?}
THROWS -->|no| FINALLY[finally block runs]
THROWS -->|yes| CATCH{Matching catch?}
CATCH -->|yes - specific type| HANDLE[Handle exception<br/>log recover rethrow]
CATCH -->|no match| PROPAGATE[Propagate up call stack]
HANDLE --> FINALLY
PROPAGATE --> FINALLY
FINALLY --> DONE[Continue or halt]
subgraph Hierarchy
THROWABLE[Throwable]
ERROR2[Error - engine errors]
EXCEPTION[Exception - app errors]
THROWABLE --> ERROR2 & EXCEPTION
end
style HANDLE fill:#238636,color:#fff
style PROPAGATE fill:#d29922,color:#fff
style FINALLY fill:#1f6feb,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Catching \Exception or \Throwable everywhere and swallowing it — silent failure is worse than a crash.
- Using exceptions for normal control flow (throwing to exit a loop) — exceptions are for exceptional situations.
- Not creating domain-specific exception classes — catching RuntimeException tells callers nothing useful.
- Logging the exception and rethrowing without passing the original as $previous — you lose the stack trace chain.
Code Examples
// Catching the base Exception hides bugs; empty catch silences everything
try {
$this->processOrder($order);
} catch (\Exception $e) {
// do nothing — you'll never know what went wrong
}
try {
$this->processOrder($order);
} catch (InsufficientStockException $e) {
return response()->json(['error' => 'Item out of stock'], 409);
} catch (PaymentDeclinedException $e) {
$this->logger->warning('Payment declined', ['order' => $order->id]);
return response()->json(['error' => 'Payment failed'], 402);
} catch (\Throwable $e) {
$this->logger->error('Unexpected error', ['exception' => $e]);
throw $e; // rethrow — let global handler deal with it
}