It's impossible to write perfect code, so we should just accept this as reality. That's why we code defensively to protect ourselves, similar to how we drive defensively.
But coders don't trust themselves either, and plan ahead to avoid their own mistakes too.
In the world of programming, this isn't paranoia. It's good thinking.
Design By Contract #
Contracts are one way we help deal with difficult human relations, specifying rights and responsibilities. This idea can be applied to how software modules interact, known as Designing By Contract.
Under this concept, a "correct" program is one that does no more and no less than it claims to do. Its rights and responsibilities are documented for other modules, like a contract.
A typical code contract will include:
- Preconditions that must be true in order for the routine to get called in the first place.
- Postconditions that guarantine the routine will conclude, and rules about its state that the result must abide by.
- Class invariants are rules that must remain true between times the routine is called. For example, a specific value must remain above X at all times. You can also have loop invariants to keep loops under control from repeating endlessly, or semantic invariants based on the modules inherent meaning or purpose.
If all the routine's preconditions are met by the caller, the routine shall guarantee that all postconditions and invariants will be true when it completes.
Failing to meet the contract terms raises an outcome to stop it, like raising an exception or ending the program. Failure to meet the contract's terms is a bug. It's better for a contract failure to crash or break the program outright than giving a vageuly incorrect or unusable value, since it's much easier to then catch and fix.
Good code contracts are strict with what they require but promise as little as possible in return. Subclasses of another code class should work off the same contract.
Dead Programs Tell No Lies #
We like to believe our code won't throw errors and deny they're possible. But errors do happen, and sometimes small errors can be a sign of a much larger error on the horizon. So it's better for an error to travel upwards and crash the whole program so it gets caught sooner.
Otherwise the ripple effects of a more subtle error can have far-reaching effects for your users or their data. Dead programs usually do a lot less damage than crippled ones.
Some languages have this built in. Those that don't can't usually have it built in to throw large errors that halt everything when needed.
There are some exceptions to this rule, but those will be addressed in a later section.
Assertive Programming #
Some use cases for our code seem so impossible, we assure ourselves they will simply never happen. But there's always a chance it will someday. No matter how certain you are, add assertions to truly ensure it won't.
Some languages have assertions built-in, and are great to use for checking data types or algorithm operations.
Some things assertions should not be or do:
- Have any side effects
- Must be kept on when compiled
- Be used in place of real error handling
- Always exit the entire program
Assertions should be left on when sent to production. Your testing suite will never be able to find all the bugs, and assertions cover many use cases the tests can't. Assertions are also your first line of defense in a far less predictable production environment. Even if they slow down performance, in which case only the most draining ones should be turned off.
When to Use Exceptions #
If your coding language allows exceptions, they're useful since you don't need to write lots of error handlers that look for as many potential errors as you can think of. Exceptions create a clearer, more logical flow of operations.
However, exceptions should be reserved for unexpected events that would make the program fail or terminate otherwise. If it's vague about if a certain outcome is a failure or not, an error return is usually better. Save exceptions for truly exceptional problems, otherwise overuse of them brings the usual lack of readability and maintainability.
How To Balance Resources #
Our code manages resources like memory, threads, timers, etc. They need to be allocated, used, then deallocated. A good rule of thumb for how our programs should manage them is "finish what you start." Any code that allocates a resource should be responsible for deallocating it. For example, and resource that opens a file should also close the file and set it free. Don't share the responsibility among different methods, since it tightly couples them and risks not closing the file in future changes.
If you need more than one resource, deallocate them in the opposite order that you allocated them in to keep any references stable. If the same resources are allocated in different places, always do so in the same order.
For some extra paranoia, add wrappers to resources to keep track of allocation and deallocations. For example, check that the number of allocations is the same afterwards as it was before, and therefore doesn't have any waiting deallocations.