Level 4: If there is a bug, it will cause an unrelated part of the program to work incorrectly. If you have only programmed in Java and other modern, high-level languages, you may have trouble thinking of a case where this would be true. In fact, errors of this sort are rare in Java, which is a tribute to some of the good decisions made in the design of the language. On the other hand, if you have done much programming in C or C++, you probably know exactly what I mean, and have spent far too much time hunting for bugs of exactly this sort.
Suppose you create an array of length 10, then try to store a value into the 11th element of the array. In Java, this will result in an ArrayIndexOutOfBoundsException. This is a level 2 bug opportunity -- not a big deal.
In contrast, C and C++ do not perform bounds checking on arrays. It will happily store the value into the memory location that would hold the 11th element of the array, if only you had allocated that much space for the array. Everything will seem to work normally, exactly as if the array really had 11 elements.
The only problem is that you have just overwritten whatever information was in that memory location. Some completely unrelated piece of code may have stored something important there. Thousands of lines further on, perhaps minutes later, that piece of code will look at that memory location and find an incorrect value there. The result could be anything from an immediate crash to a subtly incorrect result for some important calculation. Furthermore, the result could be different every time you run the program, since memory layout is not guaranteed to be the same from one run to the next.
Bugs of this sort are extremely difficult to track down, since there is no obvious connection between the error you observe and the root cause of the error. You can easily spend a week trying to fix a single bug.
You may find it surprising that I chose to categorize bug opportunities solely on the basis of how difficult it is to find the bug, not on how severe the consequences are. After all, some bugs are much worse than others. Having the program crash or corrupt user data is far worse than a minor cosmetic bug, like drawing some text in the wrong font.
The reason is that it doesn't matter how serious a bug is, as long as you find and fix it before the program is ever released to users. In practice, of course, you will have a finite amount of time for bug fixing, and may choose to focus that time on the most serious bugs, intentionally choosing to release with the less serious ones unfixed. But you first need to find the bugs before you can prioritize them. A bug that does not get found is guaranteed not to get fixed. Besides, the less time you are forced to spend finding bugs, the more time you will have left for fixing them.
Now that you know about the different types of bug opportunities, what can you do with them? First and foremost, keep them in mind while programming. You should constantly be asking yourself, "What kinds of mistakes are possible in this design?" Try to find ways to replace higher level bug opportunities with lower level ones. Above all, watch out for level 3 and 4 bug opportunities, and try to transform them into level 2 or lower ones.
Assertions are a popular technique that can be used for doing this. Suppose you have just written some code which calculates a number. You expect that number to always be positive, and if it ever were not, that would indicate that something was wrong. Then go ahead and check that in the code:
assert number > 0;
What you are doing is transforming a level 3 bug opportunity (the code works incorrectly) into a level 2 one (an exception is thrown clearly alerting you to the presence of a bug). Similarly, it is always a good idea for methods to validate their arguments to make sure whoever is calling them has passed in sensible values.
Unit tests are another popular tool for avoiding bugs. Each time you write a piece of code, you should also write a test case for it that repeatedly invokes it with different inputs and verifies that it behaves correctly. Using the classification system of this article, we can see why unit tests are so useful. They transform a level 3 bug opportunity (the code produces incorrect results) into something like a level 2 one. (It actually is slightly different: errors are not discovered when you run the program, but rather when you run the test case. As long as you run all your test cases sufficiently often, this may be just as good.)
Make extensive use of checked exceptions to signal errors as soon as they happen. Don't worry that forcing users of an API to handle exceptions might make that API harder or less convenient to use. The fact is, if a routine can fail, anyone calling that routine should be forced to think about that failure and decide how to handle it. The extra work required to write try/catch blocks is tiny compared to the cost of dealing with bugs that occur when a routine silently fails and the invoking code doesn't detect or handle it.
You cannot hope to completely eliminate all bugs from your code, but there is a lot you can do to reduce their number. Every design decision you make affects the risk of bugs later on. By analyzing the options in terms of bug opportunities and picking the design that creates the fewest, lowest level ones, you can make debugging easier and produce better, less buggy code.