Skip to content

Dead catch (catch.neverThrown) false positive around yield $key => $value - a yield with a key loses its implicit throw point #15249

Closed

Description

@ondrejmirtes

Bug report

A yield is a point where a generator can be resumed with Generator::throw(), so PHPStan gives every yield an implicit throw point, and a surrounding catch is never reported as dead. That works for yield 1;, but not when the yield has a key. yield 'a' => 1; has no throw point at all:

/** @return \Generator<string, int, mixed, void> */
function withKey(): \Generator
{
	try {
		yield 'a' => 1;
	} catch (\RuntimeException $e) {
		echo $e->getMessage();
	}
}

/** @return \Generator<int, int, mixed, void> */
function withoutKey(): \Generator
{
	try {
		yield 1;
	} catch (\RuntimeException $e) {
		echo $e->getMessage();
	}
}

$generator = withKey();
$generator->current();
$generator->throw(new \RuntimeException('caught by withKey()'));

Actual output (level 4):

8: Dead catch - RuntimeException is never thrown in the try block.   [catch.neverThrown]

The catch in withKey() is reachable. Running the script prints caught by withKey(). The identical withoutKey() gets no error. The same false positive shows up for yield $k => 1 and $x = yield 'a' => 1. It doesn't show up for yield foo() => 1, because there the key expression brings its own implicit throw point from the function call, which hides the bug.

Cause: src/Analyser/ExprHandler/YieldHandler.php starts $throwPoints with InternalThrowPoint::createImplicit($scope, $expr). When the key is present, line 70 overwrites the array instead of merging into it:

$throwPoints = $keyResult->getThrowPoints();

The value branch just below does it right ($throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints());), and so does the neighbouring $impurePoints line. The key's and the value's throw points are both kept. Only the yield's own implicit throw point is lost. The same $throwPoints also goes into VariableFlowBuilder::throws(), so anything that depends on the yield being a possible throw site (such as variable certainty in the catch block) is affected too.

Code snippet that reproduces the problem

https://phpstan.org/r/f5ac781b-381b-4b19-9f6b-1b25d80e9b2d

Expected output

No errors. yield $key => $value should keep the implicit throw point just like yield $value.

Suggested fix in YieldHandler::processExpr():

$throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints());

Did PHPStan help you today? Did it make you happy in any way?

No response

Activity

  1. added this to the Easy fixes milestone on Sep 17, 2026
  2. phpstan-bot commented on Sep 17, 2026

    @phpstan-bot
    Contributor

    Closed via merging phpstan/phpstan-src#6457

  3. github-actions commented on Oct 19, 2026

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

  4. locked as resolved and limited conversation to collaborators on Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions