Unreachable Catch Clause
ID |
php.catch_blocks_able_catch_exceptions |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Php |
Tags |
reliability, unused-code |
Description
Reports a catch clause that can never run because an earlier clause on the same try already
catches everything it would.
PHP tests the clauses top to bottom and stops at the first whose type matches the thrown object. A broader type listed above a narrower one therefore makes the narrower clause dead — and it is the dead clause that usually holds the specific, useful handling.
Rationale
try {
$this->fetch($url);
} catch (\RuntimeException $e) {
$this->logger->error('request failed');
} catch (\OutOfBoundsException $e) { // never runs
$this->retryWithSmallerPage();
}
OutOfBoundsException extends RuntimeException, so the first clause absorbs it and the retry
never happens. The code reads as if two cases were handled; one of them is not.
The trap grows with the hierarchy. catch (\Throwable $e) above anything makes every later clause
dead, because only a Throwable can reach a catch at all. A union clause has the same effect for
each type it lists:
try {
$this->fetch($url);
} catch (\RuntimeException | \LogicException $e) {
$this->logger->error('request failed');
} catch (\DomainException $e) { // never runs — covered by \LogicException above
}
Listing the same type twice is the degenerate case, and usually the residue of a merge.
Remediation
Order the clauses from most specific to most general, so each one can be reached.
try {
$this->fetch($url);
} catch (\OutOfBoundsException $e) {
$this->retryWithSmallerPage();
} catch (\RuntimeException $e) {
$this->logger->error('request failed');
}
If the two clauses turn out to do the same thing, delete the unreachable one, or merge the types into a single union clause:
try {
$this->fetch($url);
} catch (\OutOfBoundsException | \JsonException $e) {
$this->logger->error('request failed', ['reason' => $e->getMessage()]);
}
Configuration
The rule has no properties. What keeps it quiet is that it only reports a provable answer.
PHP resolves class names at run time through an autoloader, so one file rarely holds the whole
hierarchy. Coverage is decided from two certain sources only: the language’s own Throwable /
Exception / Error and SPL hierarchy, which no project can change, and the extends /
implements clauses of the types declared in the same file. As soon as a chain leaves both — a
project class extending a base from vendor/, for instance — the clause is left alone, because
that base may be unrelated to the earlier one.
Redundancy within a single clause’s type list, such as catch (\Exception | \RuntimeException
$e), is a different defect and is not reported here.