Menu

Precise try-catch-finally analysis

April 3, 2021 · 3 min read

PHPStan never used to be very precise about type inference in try-catch-finally blocks. Consider this example:

try {
$foo = doFoo();
$bar = doBar();
$baz = doBaz();
} catch (\InvalidArgumentException $e) {
// what variables are defined here?
} catch (\RuntimeException $e) {
// and here?
} finally {
// and finally... here?
}

PHPStan didn’t consider what function calls can throw which exception types. So all variables might be undefined in the catch and finally blocks as far as PHPStan was concerned.

This changes in the latest PHPStan release. It now has a very precise idea of each “throw point” in the try block and uses it to inform the type inference engine of possible types in catch and finally blocks.

/** @throws \InvalidArgumentException */
function doBar(): int
{
// ...
}

/** @throws \RuntimeException */
function doBaz(): string
{
// ...
}

try {
// $foo is surely defined in all catch blocks
// because the literal 1 doesn't throw anything
$foo = 1;

$bar = doBar(); // might throw InvalidArgumentException
$baz = doBaz(); // might throw RuntimeException
} catch (\InvalidArgumentException $e) {
// $foo is always defined
// $bar is never defined
// $baz is never defined
} catch (\RuntimeException $e) {
// $foo is always defined
// $bar is always defined
// $baz is never defined
}

By default, functions without any @throws annotation are considered to throw any exception, so all existing code will be interpreted very closely to the previous behavior.

But once you start adding @throws to your functions and methods, they will take priority over the ones with only implicit @throws. As you add more @throws annotations, you get rewarded with more precise analysis!

There will be more exception-related features in the near future, stay tuned!

© 2016–2020 Petra Mirtesová