7 Tips to Write Exceptions Everyone Will Love

InvalidArgumentException, FileNotFoundException, InternalException.

Have you ever had that feeling, that you've seen that exception before and you know what it means and how to solve? What if that would be clear even for those who see it for the first time? It would save yours and their time.

Exceptions are not just error state. Exceptions are the new documentation.

I wrote a 50-page thesis about polyphasic sleep. My opponent told me, that there is a missing part about uncontrolled intervening values. The part in pages 34-36 he probably skipped. Today we have too much going on we have to scan. Anything longer than 140 chars is exhausting. Moreover for us programmers, who dance among tsunami of information coming every hour as they code and investigate code of others.

Do you Find this "Circle of Code" Familiar?

  1. you open the application
  2. you code and code, life is great!
  3. suddenly, it's broken by InvalidStateException exception (if we're lucky)
  4. you open exception in IDE to find out more... nothing
  5. you open the documentation to find out more... nothing
  6. you Google and StackOverflow to find out more... nothing
  7. you close the application frustrated, have a ☕ or social joint to restore your will to overcome shit code

What if you could stay between 1, 2 and 3 much more often?


When people used EasyCodingStandard for their first time, they experienced many WTFs. After 30 minutes they gave up saying "coding standards are hard". When I asked them to show me how they used it, it took me 3 minutes to solve. Why? Because I made it? Well maybe. But also because exceptions were so lousy, that nobody knew how to solve them and that's wrong. Shame on me.

1. Make Exception Names for Humans

Not machines but people read exceptions. Well, machines read it to but they just log it - humans have to make code work again.

InvalidStateException

WTF? Could you be more clear?

ConfigurationFileNotFoundException

A-ha, I create ecs.yml and it works!

+10 % happier programmer

Do you need help with this? There is Sniff that makes sure no exception is generic.

2. Use " around" Statements

Filter class VeryLongNamespace\InNestedNamespace\WithMissingClassInTheEnd was not found
parameters:
    filters:
        -  VeryLongNamespace\InNestedNamespace\WithMissingClassInTheEnd

The class exists and it is autoloaded:

require_once __DIR__ . '/vendor/autolaod.php'

var_dump(class_exists(
    VeryLongNamespace\InNestedNamespace\WithMissingClassInTheEnd::class
));
// "true"

So what is wrong?


5 minutes later...

 parameters:
     filters:
-        -  VeryLongNamespace\InNestedNamespace\WithMissingClassInTheEnd
+        - VeryLongNamespace\InNestedNamespace\WithMissingClassInTheEnd

Ah, there was a space, a small single space!

You probably noticed it, because there are 3 lines of code and they get all your attention. In reality, there are 80 lines of code, 5 files opened in your IDE/brain and your colleague is asking you for wise advice, so your chances to spot this are much lower.

How to prevent this from happening ever again to anyone in the world?

Filter class VeryLongNamespace\InNestedNamespace\WithMissingClassInTheEnd was not found

Filter class " VeryLongNamespace\InNestedNamespace\WithMissingClassInTheEnd" was not found

Use quotes around every argument:

throw new FilterClassNotFoundException(sprintf(
    'Filter class "%s" was not found',
    $filterClass
));

+20 % happier programmer

3. What Exactly is Wrong?

main parameter is invalid
throw new InvalidParameterException(sprintf(
    '%s parameter is invalid.',
    $parameterName
));

What parameter?

"main" parameter is invalid
 throw new InvalidParameterException(sprintf(
-    '%s parameter is invalid.',
+    '"%s" parameter is invalid.',
     $parameterName
));

Aha! Where do I find it? In the "parameters" section?

Parameter in "parameters > page_name > main" is invalid
throw new InvalidParameterException(sprintf(
    'Parameter in "parameters > page_name > %s" is invalid.',
    $parameterName
));

Aha, now I know where to find it, thanks!

parameters:
    page_name:
        main: []

+20 % happier programmer

4. What is The Wrong Value?

Parameter in "parameters > page_name > main" is invalid

We already know where it is, but what value it actually has?

Parameter value "false" in "parameters > page_name > main" is invalid

I see, so "main" parameter can't have false value. What it can have then?

Parameter value "false" in "parameters > page_name > main" is invalid. It must be a string
if (is_array($value)) {
    $value = 'array';
} elseif (is_bool($value)) {
    $value = ($value === true) ? 'true' : 'false';
}

throw new InvalidParameterException(sprintf(
    'Parameter value "%s" in "parameters > page_name > %s" is invalid. It must be a string.',
    $value,
    $parameterName
));

+15 % happier programmer

Tip: You can use Tracy to delegate the value dumping.

5. What File Exactly is Broken?

Invalid file

In EasyCodingStandard, PHP_CodeSniffer, PHP CS Fixer, Rector or PHPStan there is always work with files. Is there some error with the file? Show it!

File /var/www/tomasvotruba.cz/packages/src/TweetPublisher.php not found

Oh, sorry:

File "/var/www/tomasvotruba.cz/packages/src/TweetPublisher.php" not found

Absolute paths can be really long in Docker, CI or in the non-basic install. We don't need irrelevant information - every character counts!

How can we make it a bit more familiar to the user? Show the relative path:

File "packages/src/TweetPublisher.php" not found

How to do this nice and lazy in PHP?

// "$filePath" can be absolute or relative; we don't care, it only must exists
$fileInfo = new SplFileInfo($filePath);

// remove absolute path start to cwd (current working directory)
$relativePath = substr($fileInfo->getRealPath(), strlen(getcwd()) + 1);

throw new FileProcessingException(sprintf(
    'File "%s" not found',
    $relativePath
));

+20 % happier programmer

6. What Options do I have?

vendor/bin/ecs check src --level laravel
"Laravel" level was not found

Don't make the programmer think!

Level "laravel" was not found. Pick one of: "array", "clean-code", "comments", "common", "control-structures", "docblock", "namespaces", "php70", "php71", "phpunit", "psr12", "psr2", "spaces", "strict", "symfony", "symfony-risky", "symplify"

That's better!

If there is the limited or reasonable amount of options, don't be shy. Show them!

$allLevels = $this->findAllLevelsInDirectory($configDirectory);

throw new LevelNotFoundException(sprintf(
    'Level "%s" was not found. Pick one of: "%s"',
    $levelName,
    implode('", "', $allLevels)
));

This code is real! It's from Symplify\PackageBuilder code.

+40 % happier programmer

Sometimes you might find yourself writing a poem instead of an exception:

throw new ConfigurationFileNotFound(
    'Class not found. Configure autoload, you can use either `parameters > autoload_files`' .
    'or `parameters > autoload_directories`. Be careful to use paths relative to the file you are using.'
);

Who would read that? Except for the author of course.

To make it shorted and readable, we end up with a lousy statement like:

throw new ConfigurationFileNotFound('Class not found. Configure autoload first.');

10 Tweets = 1 post, so just link it!

throw new ConfigurationFileNotFound(
    'Class not found. Configure autoload: https://github.com/rectorphp/rector/blob/master/README.md'
);

A bit more perfect? Use headline anchor:

throw new ConfigurationFileNotFound(
    'Class not found. Configure autoload: https://github.com/rectorphp/rector/blob/master/README.md#extra-autoloading'
);

Here is one pitfall - end of lines and line breaks in CLI. You might end up with this error message:

https://github.com/rectorphp/rector/blob/
master/README.md#extra-autoloading

Where only https://github.com/rectorphp/rector/blob/ is a link. Invalid link.

How to save this?

throw new ConfigurationFileNotFound(
    'Class not found. Configure autoload:' .
    PHP_EOL .
    'https://github.com/rectorphp/rector/blob/master/README.md#extra-autoloading'
);

Kaboom!

+30 % happier programmer


And in these 7 steps, you just made any programmer using your code 125 % happier!

What are your the most favorite exceptions?


Happy throwing!


  Travis Knowns the Code Works

The code used in this post is tested daily with Travis CI. You can see tests on Github.

Thanks to tests this post:

  • always run against the most recent dependencies
  • gets updates and stays relevant for many years even when new major version of PHP or Symfony is released

Typo? Fix it, please  and join 47 people who build this website

GitHub RSS @votrubaT Runs on Statie Hosted on GitHub Build by 48 people

Like what I write about? Hire me & we can work together