Assertion is a valuable tool that many languages provide, but recently I have come to think it is used too much and in the wrong places. I put forward the idea that asserts should not be found anywhere in the main body of your code. Permanently burying asserts deep inside your code could end up causing more problems than they seek to solve. Asserts embedded in methods are useful while debugging, but should rarely be committed to source control.
I extend this thought to other defensive practices like checking for nulls indiscriminately – even where there never should be one. Sanitization of external data is prudent and necessary, but once things are safely inside your own codebase you have control of the expected range of values and should be able to trust that.
What an assert does
An assert stops execution and flashes up a message when a condition fails. If there is a debugger attached then if it can it will direct the programmer to the source where the assert fired. Great for the programmer trying to track something down, rubbish for someone else who has to skip the message (perhaps many times) to do what they are trying to do.
Asserts disrupting workflow for other programmers and disciplines have been mitigated in a few ways. The following are some more common methods:
- Builds with asserts turned off
- Asserts with a concept of levels or categories that can be toggled
- Assert instance toggle – it will appear once but as well as skip there’s an option to ignore
Asserts that continually fire create too much noise; unless the game completely falls over then people start to not take any of them seriously. Just like compiler warnings there is a point where many eyes just glaze over the number and a number above zero becomes an excepted part of life… perhaps even becoming a joke.
Do or do not, there is no try
Asserts show a lack of confidence of an operation being successful – so much so that a human being should be warned. It is a developer communicating with themselves and others that at some point something is in a state so dire that normal execution must be interrupted so someone can take a look. I do not believe having that kind of warning permanently buried and hidden is healthy. It is preferable to either formally test and handle the situation if it really is expected, or get to the real root of the problem and prevent that invalid state from ever occurring.
Sanitize the data once properly rather than repeatedly sanitizing the same data in the same way throughout the program just in case. Every time a conditional statement is encountered there is a computational cost associated with that. A code block could have its own setup and destruction costs – perhaps more so if exception handling is being used. This may seem like premature optimization but clear concise code is easier to digest for man, compiler and machine. After all the best kind of processing is the processing you can eliminate – look to improve your algorithm.
Asserts in the middle of a method after a long conditional block could indicate that the method could be broken in to smaller more focused testable methods. If possible architect so that a method will always succeed in some measure to allow callers to expect the same predictable outcome. If a function should always return a populated something, use a default something rather than null. For example use a valid bright pink texture for missing data and log the error rather than just returning null and forcing everything from that point on to check for nulls.
Silent but deadly
A badly written assert may cause a side effect as part of testing the condition perhaps without the programmer even realizing it. When an assert like that is disabled it causes different behavior between types of builds.
Worse is when an assert expects to be heeded in the same way as exception. Ignored or absent through compilation options, execution blunders on and states become corrupted, going on to cause unpredictable problems later. Perhaps that problem will even mask itself as an issue with a completely unrelated system. In the worst case corrupt data could be persisted, ruining a saved game.
Asserts encourage a codebase that constantly tries to correct itself rather than addressing the real problem. Methods no longer trust each other to fulfill their remit. Expectations of callers change from “I should only give it foo” to “I can give it foo, bar or nothing at all”. Those expectations can virally spread throughout the codebase causing more methods to second-guess each other and bloat.
The truth is out there
Earlier I stated that asserts in the main body of code shows a lack of confidence and are used to try to mitigate bad states. Used in the context of test functions they serve to communicate an expectation of success. The tests can be run independent of running the game or application itself and provided with sample of runtime data to consume. Instead of the same assertions being made in multiple places against the same method scattered across the codebase, an expectation for a specific scenario can be stated exactly once.
Asserted expectations can be gathered in a test suite to form a living document in code that provides examples of use that are easily found. Compile time can be quicker for coders because tests can be separated out in to different projects – run only during the testing phase of a change and on the build machine.
Asserts in the main body of your code clutter the logic and could be expressed more concisely elsewhere. Programming too defensively could blur the responsibility of what is being written. Do not always code “just in case” otherwise callers will start to rely on the defensive parts. Either set the expectation of a method to always sanitize (and perhaps hint at that extra processing in the name) or fail as gracefully and quickly as possible. Methods have to be able to trust each other. Many applications and games are closed systems that have predictable points of failure, only the input from the boundaries should be considered for sanitization. Failed asserts should be a point of immediate failure – though I would argue there are better ways of indicating failure.
Asserts are very useful for debugging, but be wary of the costs of permanently committing them to the main body of your source code. Logging error messages and substituting inert values can be far less obtrusive then relying on asserts breaking execution and popping up message boxes. It is outrageous for a tool that is supposed to run unattended to stop everything waiting for input, just fail and exit.
Asserts do belong in source control when used to formally test methods and when they clearly express specifications. This kind of living documentation created by asserts is a form of cover fire. If you cannot separate asserts in to formal test methods to create clear behavior specifications, try and gather them together close to where the bad state will first occur. Group them close as possible to where an errant call could first appear on the stack, rather than scattering the expectations around deep in the code. Let the developer see everything that is expected at a glance. Asserts should help avoid detective work after all!
There are many advantages to taking asserts scattered through your code and creating test suites from them – see my previous article Cover Fire for Coders.
Some other related #AltDevBlogADay articles about debugging and asserts you might be interested in:
- Tips and Tricks for Debugging Optimized Code by Max Burke (@maximilianburke)
- Debugging Tools by Justin Liew (@justin_liew)
- Looks like I’m up by Jaymin Kessler (@okonomiyonda)
- Elegance in Failure: How and when to crash horribly by Ben Carter (@CarterBen)
- Think low level, write high level by Chris Kosanovich
- Handy Developer Tools: The Sandbox by Max Burke (@maximilianburke)
- #AltDevBlogADay programming category
- Cover Fire for Coders by Paul Evans (@paulecoyote)
Some books you might be interested in:
Chapter 8 covers the split between debugging tools and effectively handling failure in production code. It supports quite some things that I advocate in this article in a much more detailed discussion. One of my favorite quotes from the chapter: “Sometimes the best defence is a good offense. Fail hard during development so that you can fail softer during production”.
This book covers how spending time eliminating technical debt can help create a more predictable schedule and end up with a more focused, polished game.
[Paul Evans is a central technology programmer at Lionhead Studios. He has worked on the Fable II, Fable III and other unreleased titles. You can find him on Linkedin, see other things he has written on his personal blog, and follow him on twitter. Everything in this article is Paul's opinion alone and does not necessarily reflect his employers views. Copyright ©2011, Paul Evans.]
(Originally posted for #AltDevBlogADay)