There are pitfalls to Object Oriented programming. One of them is becoming so enamored with it that you apply it’s methodology when not appropriate. Recently I began (and am continuing to work on) a fault handling system which will introduce the assert statement to the Drupal codebase. One of the remaining blocks on the project is the objection of “If Symfony and the other major frameworks don’t use assert, why should we?” Well, Symfony et al are great, but they aren’t perfect. Failure to use assert and knowing when to trigger error instead of throwing an exception is one area they are lacking.
In this brief article we’ll look at the three PHP error systems which, for ease of conversation, will be collectively known as “faults”
Errors
The oldest way to create an error in PHP is trigger_error, which goes back at least as far as 3. The syntax is
trigger_error( $error_message, $error_code);
Unlike Exceptions, errors can’t be caught. I believe a lot of programmers coming to PHP from other backgrounds take note of this and write the whole mechanism off as useless, which is very short sighted. There are two things errors allow for that exceptions do not which make them ideal in certain circumstances.
First, while errors can’t be caught by try/catch blocks, they can be caught by the final global error handler of PHP. By defining code there you can do a log of the problem and perform whatever cleanup needs to be done before one of two things occurs - either the script terminates (Fatal errors) or it resumes on the next statement after the trigger_error call (Notice errors).
Fatal errors are useful because of their uncatchability. When you trigger one you can be sure the system is going down - no one using your library can write an overeager catch statement that will catch your Exception. This forces the 3rd party programmer to fix whatever is going wrong. Now, on the downside, this sort of behavior should be reserved for when it’s absolutely needed - one example would be allowing the script to proceed further in any way may lead to permanent data loss or corruption. Fatals are rare - even in an application as large as Drupal 8 I can count on one hand the number of times a fatal error should be thrown.
Notice errors get pitched for things that come up that are worth taking note of but don’t require immediate action. Impending deprecation (which has its own error code) is the most significant example and one that should be used more often than it is. Other abnormalities that can be gracefully handled but are cause for suspicion also fall to notice. These errors take advantage of the immediately resume trait that errors have.
Of all faults errors comprise of perhaps 1-2% of the codebase. Exceptions remain superior in nearly every situation, but it is important to remember the corner cases where the old trigger_error is still appropriate. Framework maintainers - please start putting E_USER_DEPRECATED calls at the start of functions you’re planning to remove. Not everyone dives into the code of every library we use to read the phpdocs.
Exceptions
throw new Exception( $exception_message );
With great power, great responsibility, and Exceptions provide a lot of fault handling power. Perhaps too much - ever tried to read code where try/catch blocks have all but supplanted if/else?
That’s perhaps the main danger of the exception handling system - letting it intrude into the world of control structures. As a rule of thumb, a library should not catch its own exceptions, and a class certainly should not. In both these cases normal control structures probably handle the situation as elegantly, and perhaps more so.
Also, where composition reigns supreme much of the time, in exceptions inheritance is more important. Exceptions rarely add anything new to the classes they descend from but the inheritance pattern classifies the exception with increasing detail. How much detail is needed is largely dependent on the application as a whole. Oddly, this classification more closely matches the understanding of classes in the world outside software.
For example: Exception > UserException > ValidationException > BadValueException
That isn’t a real sequence - just illustrative. After all it can be argued that any user caused exception is a validation exception. It can be argued that a bad value exception (as opposed to missing value) is getting too specific.
I haven’t done too much research design into this area. My instinct tells me that at a minimum each package should use it’s own exception, and other exceptions should be based either on the class they are coming from or the interface.
Exceptions should be expected to account for 90% of all faults. That said, try/catch should account for less than 10% of the program logic flow - any more than that and its likely they are being used when normal control structures would be better.
Assertion
assert( $assertion, $assert_failure_message);
So what is assert for? The first clue is in the fact that assert is normally not placed in a conditional, where exceptions and errors are. The reason for this is assertions can be turned off. This is the critical detail about them.
Exceptions and Errors are both control structures. Assertions are not. They can’t be because they can be turned off in a single call, even mid-program. They exist instead solely for the purpose of debugging the program.
Exceptions and errors exist to handle situations that might be true when the program is running as designed, no matter how unlikely they might be. Assertions exist to alert the programmer situations which should not be true. Ever. If they are, there’s a bug in the program. Why we should have assertions boils down to the following maxim.
All bugs are errors, but not all errors are bugs
User input error, database connection errors, these situations are “exceptional” Something is wrong, but these situations are not unexpected or even especially unusual in the case of user input errors.
An incoming argument is a string (yes, I know PHP 7 will allow this type hint - the point is assert can be used to type hint primitives today in PHP 5.6 ), an array argument contains specific keys and their types are correct (though why you aren’t using a parameter object if you need that level of specificity is another matter), an integer argument is between 1 and 20 for whatever reason, these are what assert is meant to test.
How does this fit in with Test Driven Design? My counter question is this:
Can you write a unit test that will test code that will be written three years from now that is calling the API you’re writing today?
Of course not.
This is where TDD fails and Assert ascends, but they are not mutually exclusive - they are tools to test for different things. TDD excels at testing the internal cohesion of a package and its units, assertions enforce the expectations of those units upon the rest of the system now and in the future. Each tool has a purpose, and the strongest code bases use both as appropriate.
It’s not appropriate to raise an exception if a bad parameter is passed unless that parameter is ultimately coming from the end user or an outside program. Since the behaviors of those two can’t be guaranteed exceptions should be used. If your library is a tool that doesn’t expect to be talking to those entities then assert should be used since they can be turned off.
This is not a matter of premature optimization either - assert has a different context for the reader than an exception block thereby documenting the code. If you see assert statement you know the author of the program does not expect whatever is being asserted to be false, if it is there’s a bug in the program that must be corrected. In contrast there’s no way of immediately knowing if an exception conditional for the same condition is something the original programmer expects to happen if the program is bug free, not without some study of the code.
Assertions also immediately call out for their correction. Take the value between 1 and 20 example, the solution is to write code in your caller to insure that value meets the expectations of the called function.
Assertions, when used responsibly, still only account for about 8% of the code, and that number will drop for PHP 7 since one of their main current uses - return type enforcement - will be subsumed by new language features.
Conclusion
Three tools with three different applications. Exceptions are the hammer and nails of PHP - don’t use them when screws (errors) and temporary bracing (assert) are called for.