psalm: Idea: `@psalm-never-throw`, alike to `@psalm-pure`, but promises to not throw/warn

Similar to @psalm-pure, it may be interesting to have something like @psalm-never-throw, in which the guarantees are:

  1. the code does not kill the process (yes, the engine can still go out of memory or out of stack frames)
  2. the code does not raise any notices/warnings
  3. the code does not throw exceptions
  4. all thrown exceptions are caught

This is interesting when declaring interfaces with strong guarantees, such as:

/** @psalm-never-throw */
interface Worker
{
    function process(Queue $queue): void;
}

The code in any Worker#run() implementations must therefore only rely on other @psalm-never-throw implementations, or catch any exceptions declared in code that has @throws.

Triggering errors/warnings, exit, die(), as well as the throw construct are forbidden in classes/functions annotated with @psalm-never-throw.

No further guarantees are really possible with the language, but this should play nicely with libraries such as webmozart/assert, or thecodingmachine/safe.

Again, feel free to shoot this down: I’m only throwing this out here as an idea 😉

A list of examples where this is useful:

  1. components such as event listeners (hooks): event listeners are loathed because they can crash the caller in most traditional PHP installations, breaking the entire workflow
  2. process managers/policies, such as [Event] -> IO [Command], which should not crash the caller
  3. workers, which have the responsibility (best effort) of staying alive
  4. event loops
  5. error handlers, which should not themselves produce more errors

EDIT: as highlighted by @ondrejmirtes in https://github.com/vimeo/psalm/issues/2912#issuecomment-595128822, @throws void is a viable alternative syntax

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 14
  • Comments: 18 (11 by maintainers)

Most upvoted comments

@bdsl Yes, this distinction is called checked/unchecked exceptions. You should declare throws and be catching/rethrowing exceptions that are interesting for the application logic and are expected to happen. Something like CustomerCanNoLongerBuyThisProductException.

Unchecked exceptions are for stuff that can break but shouldn’t be handled in code, but instead prevented by validations, or fixed in infrastructure. This for example includes PDOException (when the database disconnects) or NegativeIntegerException.

@letnando @psalm-safe seems easily confused with @psalm-pure. For me, a method that does not throw, yet does modify global state, is not safe 🤷‍♀️

Good idea, the naming you proposed is more verbose but I would propose @psalm-safe, meaning that’s safe to be called without throwing exceptions/errors. What do you think? 🤔

I would prefer @psalm-never-throw over @throws void.

PHPStan uses @throws void for this. For example if the parent method has some @throws Exception and you don’t want to inherit these tags, you need to use @throws void above the overriden method.

In that case this really is identical to @psalm-immutable/@psalm-pure in terms of enforcement – if I’m reading it correctly the property must be on every single call:

/**
 * @psalm-never-throws
 */
class SomeClass {
  public function someMethod(Foo $foo) {
    $foo->thisMethodMustHaveANeverThrowAnnotation();
    echo (string) $foo; // so should this  __toString()
    echo $foo->aMagicGetWouldNeedItToo;
  }
}

I can see the actual downstream enforcement of that being a bit of a slog – you’d have to make a potentially non-trivial amount of code changes to pass these checks (the same is obviously true of pure/immutable annotations, but at least that cost/benefit ratio is widely understood by the FP community).

This is not something I’m likely to want to implement in Psalm for myself, but I’ll gladly accept a PR – I just created a special mattwontfix label. For this and other similar things where I invite others’ contributions!

@M1ke I don’t think I’d expect @throws to be a gurantee that nothing else will be thrown - I only use @throws to declare exceptions that I think the imediate caller might be interested in catching or redeclaring. For instance I wouldn’t generally declare @throws Doctrine\DBAL\Exception on a repository.

For that case would it just make sense to treat @throws as that guarantee anyway? Like, if you’re saying @throws FileNotFoundException but your code can also throw IllegalArgumentException you’re being misleading surely? (at risk of taking @Ocramius’ point off topic…)

Ah no, I didn’t mean that a psalm-throw-only function must not return - I meant that it must not throw or exit except by throwing one of its declared exceptions. It can still return.