Inside a PHP namespace, why can a catch (Exception $e) block silently miss a thrown built-in Exception?
answer
- class names never fall back to global
- Exception becomes App\Exception
- catch never autoloads
- unknown catch class: no error
- use Exception; or a leading backslash
basics
~20 sInside a namespace an unqualified class name resolves to that namespace, so catch (Exception $e) in namespace App means App\Exception. catch never autoloads or reports a missing class; the clause simply never matches and the exception propagates.
solid answer
~40 sInside `namespace App\Billing;`, the name `Exception` is resolved at compile time to `App\Billing\Exception`. Unlike functions and constants, class names have no fallback to the global namespace. At run time the `catch` clause looks that class up **without autoloading**, and a class that does not exist simply matches nothing — no error, no warning. So a thrown `\Exception` or `\RuntimeException` sails past the block and surfaces somewhere else, often as an uncaught error far from the code that was meant to handle it. The fix is `catch (\Exception $e)` or a `use Exception;` import at the top of the file. The same silent miss happens with a typo or a forgotten `use` for your own exception class, which is why PHPStan reports a caught class that does not exist.
code
php · 14 lines<?php
namespace App\Billing;
function refund(): void
{
throw new \RuntimeException('refund window closed');
}
try {
refund();
} catch (Exception $e) { // means App\Billing\Exception, which does not exist
echo "handled\n"; // never runs
}
// Fatal error: Uncaught RuntimeException: refund window closedgo deeper
Recall that inside a namespace you write \Exception or add use Exception; before catching a built-in class.
Explain that class names resolve to the current namespace without fallback, and that catch looks classes up without autoloading or reporting a miss.
Diagnose an uncaught exception thrown through code that visibly catches it, and make static analysis and error-path tests catch the miss in CI.
Decide how the codebase enforces name hygiene — imports, fully qualified built-ins, analyser levels — so a namespace migration cannot quietly break error handling.
## How PHP resolves class names in a namespace Once a file declares `namespace App\Billing;`, every class name written in it is resolved relative to that namespace unless it is imported or fully qualified: | Written as | Resolves to | Notes | |---|---|---| | `Exception` | `App\Billing\Exception` | Unqualified: current namespace, no fallback | | `Gateway\Timeout` | `App\Billing\Gateway\Timeout` | Qualified: still relative to the current namespace | | `\Exception` | `Exception` (the global class) | Fully qualified: used as written | | `Exception` after `use Exception;` | `Exception` (the global class) | The import rewrites the short name | The PHP manual is explicit that **class names always resolve to the current namespace name**. The fallback to the global space that many people remember applies only to **functions and constants**: an unqualified `strlen()` inside a namespace falls back to the global `strlen()` if no namespaced one exists. Classes get no such rescue. So in a namespaced file, `new Exception('x')` fails loudly with a "class not found" error — but a `catch` written the same way does not. ## Why the catch clause stays silent When an exception reaches a `catch (Exception $e)` clause, the engine looks up the named class **without triggering the autoloader and without reporting a failure**. The reasoning is sound: if a class has never been loaded, no object of that class can exist, so it cannot possibly be the thrown object — calling the autoloader just to answer "no" would waste work. The same no-autoload lookup is used by `instanceof`. The consequence is that a `catch` naming a non-existent class is legal, compiles, and never matches: 1. The thrown object is a global `\RuntimeException`. 2. The clause asks: is it an instance of `App\Billing\Exception`? That class does not exist, so the answer is no. 3. The next clause is tried; if none matches, the exception propagates as if the `try` had no `catch` at all. ## What it looks like in production - A "handled" failure shows up as an uncaught exception in logs, pointing at code that clearly has a `catch` around it. - The bug appears only after code moves into a namespace, or after a PSR-4 refactor adds `namespace` lines to old files. - A `catch (PaymentDeclined $e)` with a forgotten `use` for a class that lives in another namespace behaves the same way, and a typo in the class name does too. ## How to prevent it - Write global classes with a **leading backslash** in namespaced code: `catch (\Exception $e)`, `catch (\JsonException $e)`. - Or **import** them at the top of the file: `use Exception;`, `use JsonException;`. - Run a static analyser in CI. PHPStan reports `Caught class ... not found.` for a `catch` naming a class it cannot find, which turns this silent runtime miss into a build failure. - Cover the error path with a test that actually throws, so a clause that never matches fails the test instead of passing unnoticed. ## When moving old code into a namespace Adding a `namespace` line to a file written in the global space changes the meaning of every unqualified class name in it at once. A safe migration: 1. List every class name the file uses without a leading backslash — in `new`, `catch`, `instanceof`, type declarations and `::class`. 2. For built-in classes such as `Exception`, `DateTimeImmutable`, `PDO` or `ArrayObject`, add a `use` import or a leading backslash. 3. Run the static analyser: `new` and type declarations fail loudly at run time anyway, but `catch` and `instanceof` do not. 4. Run the tests that exercise error paths, not only the happy path. ## The same trap with instanceof `$e instanceof Exception` inside a namespace has the same resolution rule and the same silent lookup: it is simply `false` when `App\Billing\Exception` does not exist. Any code that classifies throwables by type — a retry policy, an error mapper — can misroute failures for the same reason.
- Why does new Exception('x') fail loudly in the same namespaced file while catch (Exception $e) fails silently?Creating an object needs the class, so `new` runs the autoloader and throws an `Error` when `App\Billing\Exception` cannot be found. A `catch` clause only needs to answer whether the thrown object belongs to that class, and a class that was never loaded cannot have instances, so the engine looks it up without autoloading and treats a missing class as no match.
- Does the global fallback that makes strlen() work inside a namespace also apply to class names?No. Inside a namespace, unqualified function and constant names fall back to the global ones when no namespaced version exists. Class names never fall back: they always resolve to the current namespace unless imported with `use` or written with a leading backslash.
saying these in an interview costs you the question
- PHP falls back to the global Exception when the namespaced one is missing
- A catch naming an unknown class throws a class-not-found error
- The autoloader is called for the class named in a catch clause
- The global fallback for functions applies to class names too