Skip to content

Conditional throw-types #7906

Closed
phpstan/phpstan-src
#5874

Description

@VincentLanglet

Feature request

Follow up of #7188
as asked in #7188 (comment)

phpstan/phpstan-src#1378 (comment) was only reported as a partial solution.

The purpose of this issue is to support the syntax
https://phpstan.org/r/b3032b8e-3f03-4cc0-848f-2437a24de0d5

/**
 * @param int $x
 * @throws Exception
 * @return ($x is 0 ? never : float)
 */
function inverse($x): float
{
	if ($x===0) {
		throw new Exception('Division durch Null.');
	}

	return 1/$x;
}

function () {
	$result = inverse(0);
	echo "expect to unreachable\n";
};

allow to report the unreachable code ; but the purpose of the conditional throws type would be having phpstan to report (when using the exceptions config to check everything about exceptions)

  • A missing throws tag for (already done)
function () {
	$result = inverse(0);
};

or

function ($x) {
	$result = inverse($x);
};
  • No missing throws tag for
function () {
	$result = inverse(3);
};

or

/** param int<3, 5> $x
function ($x) {
	$result = inverse($x);
};

Activity

  1. mhsdesign commented on Oct 26, 2024

    @mhsdesign

    i currently have the same need when having a command handler with a ->handle function and various commands. Only few commands could possibly lead to an exception and for others its never thrown. Thus i think it would be interesting to have. Like described in #7188

  2. ALameLlama commented on Aug 11, 2025

    @ALameLlama

    I have a function that can take either an array or a string, if it's a string it will try and do a json_decode which can throw with the JSON_THROW_ON_ERROR flag.

    It would be nice to be able to show that this function can throw if it's getting passed a string but suppress the throw warning when it's an array.

  3. shaedrich commented on Dec 30, 2025

    @shaedrich

    I don't think, we can modify the existing native @throws annotation, because it only indicates that it may throw, not that it definitely does. Neither do conditional return types with never or assertions help us achieve PHPStan definitely knowing when an exception is thrown (when this can can be expressed declaratively, of course). But the latter may be a good inspiration

    I think, this can be particularly helpful with multiple exceptions (sorry if the example is a bit contrived, but you'll get the idea):

    /**
     * @template TKeyType of string|int
     */
    class Model
    {
        protected static string $keyType = 'int';
    
        /**
         * @param TKeyType $id
         * @return ($id is TKeyType ? ($id is negative-int ? never : \Model) never)
         * 
         * @throws-if-false \TypeError $id is TKeyType
         * @throws-if-true \OutOfRangeException $id is negative-int 
         */
        public static function fetch(string|int $id): Model
        {
            if (gettype($id) !== self::keyType) {
                throw new TypeError(sprint(
                    'The ID\'s type must match the model key type of %s, %s given',
                    self::keyType,
                    gettype($id),
                );
            }
            if ($this->keyType === 'int' && $id < 0) {
                throw new OutOfRangeException(sprint(
                    'Integer IDs must have a positive value',
                    $this->keyType,
                    gettype($id),
                );
            }
    
            return ModelRepository::find($id);
        }
    }
  4. sitepark-schaeper commented on Jan 6, 2026

    @sitepark-schaeper

    I'd like to contribute a pattern I've encountered a couple of times.
    When methods have different behaviour based on optional or uniontype arguments, one of those may throw while others don't.

    In this example registerHandler can only throw when an array is passed, but not if a Handler instance is:

    public class Manager
    {
        /**
         * @param Handler|array{type: string, options?: array<string, scalar>} $handler
         * @throws HandlerInstantiationException if the handler cannot be constructed
         */
        public function registerHandler(Handler|array $handler): void
        {
            if (is_array($handler)) {
                $handler = $this->handlerFactory->fromArray($handler); // can throw
            }
            $this->handlers[] = $handler;
        }
    }

    Idealy I'd like phpstan to report an unhandled exception here:

        public function registerMyHandler(): void
        {
            $this->manager->registerHandler(['type' => 'myHandler']);
        }

    But not here:

        public function registerMyOtherHandler(): void
        {
            $this->manager->registerHandler(new Handler('myHandler'));
        }
  5. staabm commented on Jan 6, 2026

    @staabm
    Contributor
  6. sitepark-schaeper commented on Jan 8, 2026

    @sitepark-schaeper

    @sitepark-schaeper I think what you need is documented here: https://phpstan.org/developing-extensions/dynamic-throw-type-extensions

    Oh indeed, that does the trick. I didn't think about building a custom extension for this, thank you.

    However, all of these cases could probably be solved with custom extensions, right? If I understand this issue correct it's about supporting a "native" syntax - which would also apply to the cases I mentioned. So while I can solve our issue now I think there's still a case to be made for it to be respected in an eventual implementation.

  7. shaedrich commented on Jan 8, 2026

    @shaedrich

    Exactly, writing imperative code to achieve this is possible but a considerable overhead as compared to one declarative annotation line within a PHPDoc comment

  8. phpstan-bot commented on Sep 20, 2026

    @phpstan-bot
    Contributor

    Closed via merging phpstan/phpstan-src#5874

  9. github-actions commented on Oct 22, 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.

  10. locked as resolved and limited conversation to collaborators on Oct 22, 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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions