PHPackages                             mazatian/phpstan-exception-rules - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. mazatian/phpstan-exception-rules

ActivePhpstan-extension[Testing &amp; Quality](/categories/testing)

mazatian/phpstan-exception-rules
================================

Exception rules for PHPStan

v1.0.6(11mo ago)010MITPHPPHP &gt;=8.4

Since Jun 5Pushed 11mo agoCompare

[ Source](https://github.com/mazatian/phpstan-exception-rules)[ Packagist](https://packagist.org/packages/mazatian/phpstan-exception-rules)[ RSS](/packages/mazatian-phpstan-exception-rules/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (7)Dependencies (11)Versions (8)Used By (0)

PHPStan exception rules
=======================

[](#phpstan-exception-rules)

[![Build Status](https://camo.githubusercontent.com/67a60e12cfa8ba54fb0b709e3b692eccfa0f7c76b0186e2d16da033549b16dd6/68747470733a2f2f7472617669732d63692e6f72672f706570616b72697a2f7068707374616e2d657863657074696f6e2d72756c65732e737667)](https://travis-ci.org/pepakriz/phpstan-exception-rules)[![Latest Stable Version](https://camo.githubusercontent.com/ab8ac5baef522b96959e7fad378bf23156c59122b785d8924c15b3428162762a/68747470733a2f2f706f7365722e707567782e6f72672f706570616b72697a2f7068707374616e2d657863657074696f6e2d72756c65732f762f737461626c65)](https://packagist.org/packages/pepakriz/phpstan-exception-rules)[![License](https://camo.githubusercontent.com/7b93a2c06e4a0ed2d7416b5ec45912fe290d6ee07810a7f7e505bbf148c8dc3d/68747470733a2f2f706f7365722e707567782e6f72672f706570616b72697a2f7068707374616e2d657863657074696f6e2d72756c65732f6c6963656e7365)](https://packagist.org/packages/pepakriz/phpstan-exception-rules)

- [PHPStan](https://phpstan.org/)

This extension provides following rules and features:

- Require `@throws` annotation when some checked exception is thrown ([examples](https://github.com/pepakriz/phpstan-exception-rules/blob/master/tests/src/Rules/data/throws-annotations.php))
- Exception propagation over:
    - Function calls
    - Magic, dynamic and static method calls
    - Iterable interface in foreach and in `iterator_*()` functions ([examples](https://github.com/pepakriz/phpstan-exception-rules/blob/master/tests/src/Rules/data/iterators.php))
    - Countable interface combinated with `count()` function ([examples](https://github.com/pepakriz/phpstan-exception-rules/blob/master/tests/src/Rules/data/countables.php))
    - JsonSerializable interface combinated with `json_encode()` function ([examples](https://github.com/pepakriz/phpstan-exception-rules/blob/master/tests/src/Rules/data/json-serializable.php))
- Ignore caught checked exceptions ([examples](https://github.com/pepakriz/phpstan-exception-rules/blob/master/tests/src/Rules/data/try-catch.php))
- Unnecessary `@throws` annotation detection ([examples](https://github.com/pepakriz/phpstan-exception-rules/blob/master/tests/src/Rules/data/unused-throws.php))
- Useless `@throws` annotation detection ([examples](https://github.com/pepakriz/phpstan-exception-rules/blob/master/tests/src/Rules/data/useless-throws.php))
- Optionally allows unused `@throws` annotations in subtypes ([examples](https://github.com/pepakriz/phpstan-exception-rules/blob/master/tests/src/Rules/data/intentionally-unused-throws.php))
- `@throws` annotation variance validation ([examples](https://github.com/pepakriz/phpstan-exception-rules/blob/master/tests/src/Rules/data/throws-inheritance.php))
- [Dynamic throw types based on arguments](#extensibility)
- Unreachable catch statements
    - exception has been caught in some previous catch statement ([examples](https://github.com/pepakriz/phpstan-exception-rules/blob/master/tests/src/Rules/data/unreachable-catches.php))
    - exception has been caught twice in the same catch statement ([examples](https://github.com/pepakriz/phpstan-exception-rules/blob/master/tests/src/Rules/data/dead-catch-union.php))
    - checked exception is never thrown in the corresponding try block ([examples](https://github.com/pepakriz/phpstan-exception-rules/blob/master/tests/src/Rules/data/unused-catches.php))
- Report throwing checked exceptions in the global scope ([examples](https://github.com/pepakriz/phpstan-exception-rules/blob/master/tests/src/Rules/data/throws-in-global-scope.php))

Features and rules provided by PHPStan core (we rely on):

- `@throws` annotation must contain only valid `Throwable` types
- Thrown value must be subclass of `Throwable`

Usage
-----

[](#usage)

To use this extension, require it in [Composer](https://getcomposer.org/):

```
composer require --dev pepakriz/phpstan-exception-rules
```

And include and configure extension.neon in your project's PHPStan config:

```
includes:
	- vendor/pepakriz/phpstan-exception-rules/extension.neon

parameters:
	exceptionRules:
		reportUnusedCatchesOfUncheckedExceptions: false
		reportUnusedCheckedThrowsInSubtypes: false
		reportCheckedThrowsInGlobalScope: false
		checkedExceptions:
			- RuntimeException
```

You could use `uncheckedExceptions` when you prefer a list of unchecked exceptions instead. It is a safer variant, but harder to adapt to the existing project.

```
parameters:
	exceptionRules:
		uncheckedExceptions:
			- LogicException
			- PHPUnit\Framework\Exception
```

> `checkedExceptions` and `uncheckedExceptions` cannot be configured at the same time

If some third-party code defines wrong throw types (or it doesn't use @throws annotations at all), you could override definitions like this:

```
parameters:
	exceptionRules:
		methodThrowTypeDeclarations:
			FooProject\SomeService:
				sendMessage:
					- FooProject\ConnectionTimeoutException
				methodWithoutException: []
		functionThrowTypeDeclarations:
			myFooFunction:
				- FooException
```

In some cases, you may want to ignore exception-related errors as per class basis, as is usually the case for testing:

```
parameters:
	exceptionRules:
		methodWhitelist:
			PHPUnit\Framework\TestCase: '#^(test|(setup|setupbeforeclass|teardown|teardownafterclass)$)#i'
```

Extensibility
-------------

[](#extensibility)

`Dynamic throw type extensions` - If the throw type is not always the same, but depends on an argument passed to the method. (Similar feature as [Dynamic return type extensions](https://phpstan.org/developing-extensions/dynamic-return-type-extensions))

There are interfaces, which you can implement:

- `Pepakriz\PHPStanExceptionRules\DynamicMethodThrowTypeExtension` - service tag: `exceptionRules.dynamicMethodThrowTypeExtension`
- `Pepakriz\PHPStanExceptionRules\DynamicStaticMethodThrowTypeExtension` - service tag: `exceptionRules.dynamicStaticMethodThrowTypeExtension`
- `Pepakriz\PHPStanExceptionRules\DynamicConstructorThrowTypeExtension` - service tag: `exceptionRules.dynamicConstructorThrowTypeExtension`
- `Pepakriz\PHPStanExceptionRules\DynamicFunctionThrowTypeExtension` - service tag: `exceptionRules.dynamicFunctionThrowTypeExtension`

and register as service with correct tag:

```
services:
	-
		class: App\PHPStan\EntityManagerDynamicMethodThrowTypeExtension
		tags:
			- exceptionRules.dynamicMethodThrowTypeExtension
```

Motivation
----------

[](#motivation)

There are 2 types of exceptions:

1. Safety-checks that something should never happen (you should never call some method in some case etc.). We call these [**LogicException**](http://php.net/manual/en/class.logicexception.php) and if they are thrown, programmer did something wrong. For that reason, it is important that this exception is never caught and kills the application. Also, it is important to write good descriptive message of what went wrong and how to fix it - that is why every LogicException must have a message. Therefore, inheriting LogicException does not make much sense. Also, LogicException should never be `@throws` annotation (see below).
2. Special cases in business logic which should be handled by application and error cases that just may happen no matter how hard we try (e.g. HTTP request may fail). These exceptions we called [**RuntimeException**](http://php.net/manual/en/class.runtimeexception.php) or maybe better "checked exception". All these exceptions should be checked. Therefore it must be either caught or written in `@throws` annotation. Also if you call an method with that annotation and do not catch the exception, you must propagate it in your `@throws` annotation. This, of course, may spread quickly. When this exception is handled (caught), it is important for programmer to immediately know what case is handled and therefore all used RuntimeExceptions are inherited from some parent and have very descriptive class name (so that you can see it in catch construct) - for example `CannotCloseAccountWithPositiveBalanceException`. The message is not that important since you should always catch these exceptions somewhere, but in our case we often use that message in API output and display it to end-user, so please use something informative for users in that cases (you can pass custom arguments to constructor (e.g. entities) to provide better message). Sometimes you can meet a place where you know that some exception will never be thrown - in this case you can catch it and wrap to LogicException (because when it is thrown, it is a programmer's fault).

It is always a good idea to wrap previous exception so that we do not lose information of what really happened in some logs.

```
// no throws annotation
public function decide(int $arg): void
{
	switch ($arg) {
		case self::ONE:
			$this->decided()
		case self::TWO:
			$this->decidedDifferently()
		default:
			throw new LogicException("Decision cannot be made for argument $arg because of ...");
	}
}

/**
 * @return mixed[]
 *
 * @throws PrintJobFailedException
 */
private function sendRequest(Request $request): array
{
	try {
		$response = $this->httpClient->send($request);
		return Json::decode((string) $response->getBody(), Json::FORCE_ARRAY);

	} catch (GuzzleException | JsonException $e) {
		throw new PrintJobFailedException($e);
	}
}

class PrintJobFailedException extends RuntimeException
{

	public function __construct(Throwable $previous)
	{
		parent::__construct('Printing failed, remote printing service is down. Please try again later', $previous);
	}

}
```

Known limitations
-----------------

[](#known-limitations)

#### Anonymous functions are analyzed at the same place they are declared

[](#anonymous-functions-are-analyzed-at-the-same-place-they-are-declared)

*False positive when a method does not execute declared function:*

```
/**
 * @throws FooRuntimeException false positive
 */
public function createFnFoo(int $arg): callable
{
	return function () {
		throw new FooRuntimeException();
	};
}
```

*But most of use-cases just works:*

```
/**
 * @param string[] $rows
 * @return string[]
 *
 * @throws EmptyLineException
 */
public function normalizeRows(array $rows): array
{
	return array_map(function (string $row): string {
		$row = trim($row);
		if ($row === '') {
			throw new EmptyLineException();
		}

		return $row;
	}, $rows);
}
```

#### `Catch` statement does not know about runtime subtypes

[](#catch-statement-does-not-know-about-runtime-subtypes)

This case is detected by rule, so you will be warned about a potential risk.

*Runtime exception is absorbed:*

```
// @throws phpdoc is not required
public function methodWithoutThrowsPhpDoc(): void
{
	try {
		throw new RuntimeException();
		$this->dangerousCall();

	} catch (Throwable $e) {
		throw $e;
	}
}
```

*As a workaround you could use custom catch statement:*

```
/**
 * @throws RuntimeException
 */
public function methodWithThrowsPhpDoc(): void
{
	try {
		throw new RuntimeException();
		$this->dangerousCall();

	} catch (RuntimeException $e) {
		throw $e;
	} catch (Throwable $e) {
		throw $e;
	}
}
```

###  Health Score

34

—

LowBetter than 77% of packages

Maintenance52

Moderate activity, may be stable

Popularity5

Limited adoption so far

Community15

Small or concentrated contributor base

Maturity59

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 80.8% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

7

Last Release

340d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b6eef13b5cb239445bcfe63f2bc77cd826fb8e08a44c031623c8690e63b07a36?d=identicon)[mazatian](/maintainers/mazatian)

---

Top Contributors

[![pepakriz](https://avatars.githubusercontent.com/u/383294?v=4)](https://github.com/pepakriz "pepakriz (143 commits)")[![VincentLanglet](https://avatars.githubusercontent.com/u/9052536?v=4)](https://github.com/VincentLanglet "VincentLanglet (13 commits)")[![marcospassos](https://avatars.githubusercontent.com/u/943036?v=4)](https://github.com/marcospassos "marcospassos (12 commits)")[![ondrejmirtes](https://avatars.githubusercontent.com/u/104888?v=4)](https://github.com/ondrejmirtes "ondrejmirtes (1 commits)")[![rmikalkenas](https://avatars.githubusercontent.com/u/14221532?v=4)](https://github.com/rmikalkenas "rmikalkenas (1 commits)")[![solcik](https://avatars.githubusercontent.com/u/1543737?v=4)](https://github.com/solcik "solcik (1 commits)")[![Stadly](https://avatars.githubusercontent.com/u/7263579?v=4)](https://github.com/Stadly "Stadly (1 commits)")[![szepeviktor](https://avatars.githubusercontent.com/u/952007?v=4)](https://github.com/szepeviktor "szepeviktor (1 commits)")[![jakzal](https://avatars.githubusercontent.com/u/190447?v=4)](https://github.com/jakzal "jakzal (1 commits)")[![xificurk](https://avatars.githubusercontent.com/u/117465?v=4)](https://github.com/xificurk "xificurk (1 commits)")[![janatjak](https://avatars.githubusercontent.com/u/11320085?v=4)](https://github.com/janatjak "janatjak (1 commits)")[![localheinz](https://avatars.githubusercontent.com/u/605483?v=4)](https://github.com/localheinz "localheinz (1 commits)")

###  Code Quality

TestsPHPUnit

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/mazatian-phpstan-exception-rules/health.svg)

```
[![Health](https://phpackages.com/badges/mazatian-phpstan-exception-rules/health.svg)](https://phpackages.com/packages/mazatian-phpstan-exception-rules)
```

###  Alternatives

[vimeo/psalm

A static analysis tool for finding errors in PHP applications

5.8k77.5M6.7k](/packages/vimeo-psalm)[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.4k43.5M5.2k](/packages/larastan-larastan)[behat/behat

Scenario-oriented BDD framework for PHP

4.0k96.8M2.0k](/packages/behat-behat)[phpstan/phpstan-symfony

Symfony Framework extensions and rules for PHPStan

78268.9M1.5k](/packages/phpstan-phpstan-symfony)[phpstan/phpstan-doctrine

Doctrine extensions for PHPStan

66466.6M1.1k](/packages/phpstan-phpstan-doctrine)[spaze/phpstan-disallowed-calls

PHPStan rules to detect disallowed method &amp; function calls, constant, namespace, attribute, property &amp; superglobal usages, with powerful rules to re-allow a call or a usage in places where it should be allowed.

33120.0M374](/packages/spaze-phpstan-disallowed-calls)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
