- Topic 23: Design by Contract
- Topic 24: Dead Programs Tell No Lies
- Topic 25: Assertive Programming
- Topic 26: How to Balance Resources
- Topic 27: Don't Outrun Your Headlights
Topic 25: Assertive Programming
There is a luxury in self-reproach. When we blame ourselves we feel no one else has a right to blame us.
Oscar Wilde, The Picture of Dorian Gray
It seems that there’s a mantra that every programmer must memorize early in his or her career. It is a fundamental tenet of computing, a core belief that we learn to apply to requirements, designs, code, comments, just about everything we do. It goes
This can never happen…
“This application will never be used abroad, so why internationalize it?” “count can’t be negative.” “Logging can’t fail.”
Let’s not practice this kind of self-deception, particularly when coding.
Whenever you find yourself thinking “but of course that could never happen,” add code to check it. The easiest way to do this is with assertions. In many language implementations, you’ll find some form of assert that checks a Boolean condition.2 These checks can be invaluable. If a parameter or a result should never be null, then check for it explicitly:
assert (result != null);
In the Java implementation, you can (and should) add a descriptive string:
assert result != null && result.size() > 0 : "Empty result from XYZ"
Assertions are also useful checks on an algorithm’s operation. Maybe you’ve written a clever sort algorithm, named my_sort. Check that it works:
books = my_sort(find("scifi")) assert(is_sorted?(books))
Don’t use assertions in place of real error handling. Assertions check for things that should never happen: you don’t want to be writing code such as the following:
puts("Enter 'Y' or 'N': ") ans = gets[0] # Grab first character of response assert((ch == 'Y') || (ch == 'N')) # Very bad idea!
And just because most assert implementations will terminate the process when an assertion fails, there’s no reason why versions you write should. If you need to free resources, catch the assertion’s exception or trap the exit, and run your own error handler. Just make sure the code you execute in those dying milliseconds doesn’t rely on the information that triggered the assertion failure in the first place.
Assertions and Side Effects
It’s embarrassing when the code we add to detect errors actually ends up creating new errors. This can happen with assertions if evaluating the condition has side effects. For example, it would be a bad idea to code something such as
while (iter.hasMoreElements()) { assert(iter.nextElement() != null); Object obj = iter.nextElement(); // .... }
The .nextElement() call in the assertion has the side effect of moving the iterator past the element being fetched, and so the loop will process only half the elements in the collection. It would be better to write
while (iter.hasMoreElements()) { Object obj = iter.nextElement(); assert(obj != null); // .... }
This problem is a kind of Heisenbug3—debugging that changes the behavior of the system being debugged.
(We also believe that nowadays, when most languages have decent support for iterating functions over collections, this kind of explicit loop is unnecessary and bad form.)
Leave Assertions Turned On
There is a common misunderstanding about assertions. It goes something like this:
Assertions add some overhead to code. Because they check for things that should never happen, they’ll get triggered only by a bug in the code. Once the code has been tested and shipped, they are no longer needed, and should be turned off to make the code run faster. Assertions are a debugging facility.
There are two patently wrong assumptions here. First, they assume that testing finds all the bugs. In reality, for any complex program you are unlikely to test even a minuscule percentage of the permutations your code will be put through. Second, the optimists are forgetting that your program runs in a dangerous world. During testing, rats probably won’t gnaw through a communications cable, someone playing a game won’t exhaust memory, and log files won’t fill the storage partition. These things might happen when your program runs in a production environment. Your first line of defense is checking for any possible error, and your second is using assertions to try to detect those you’ve missed.
Turning off assertions when you deliver a program to production is like crossing a high wire without a net because you once made it across in practice. There’s dramatic value, but it’s hard to get life insurance.
Even if you do have performance issues, turn off only those assertions that really hit you. The sort example above may be a critical part of your application, and may need to be fast. Adding the check means another pass through the data, which might be unacceptable. Make that particular check optional, but leave the rest in.
Exercise 16 (possible answer)
A quick reality check. Which of these “impossible” things can happen?
A month with fewer than 28 days
Error code from a system call: can’t access the current directory
In C++: a = 2; b = 3; but (a + b) does not equal 5
A triangle with an interior angle sum ≠ 180°
A minute that doesn’t have 60 seconds
(a + 1) <= a
Related Sections Include
Topic 23, Design by Contract
Topic 24, Dead Programs Tell No Lies
Topic 42, Property-Based Testing
Topic 43, Stay Safe Out There