My answer should be considered complementary to the other answers. There are additional things you can do to reduce time spent debugging. Efficient programmers spend little time debugging, primarily because most bugs that occur are caught quickly, where they are easy to fix, not deep in a call chain, and not by a customer or a colleague.
Tips:
- As scriptin said, avoid mutating data: if you language has keywords to prevent a variable or field from mutating (being assigned a new value), use those keywords as much as possible. The default state for all fields should be immutable, modified to mutable only when needed.
- When you must mutate data, keep the range of mutation (range of possible values) as small as possible. Identifying invariants (described below) will help. The purpose of this is to reduce the state space of the program, which simplifies proofs, testing, and debugging.
- Minimize the number of places when a specific mutable field is mutated.
- Minimize the number of points in time when fields are mutated. Functional programming approaches will naturally guide you in this direction.
- Simplify code as much as possible by collecting common code into methods, functions, or base classes.
- Keep the length of the code as short as possible, and avoid over-engineering, by avoiding cargo-cult design techniques. Superfluous design patterns and "principles", applied indiscriminately, are the primary culprits.
- Use informal proofs at each level of abstraction, or at least for the most critical sections at the lowest levels.
To use informal proofs you must identify three things, repeatedly:
- Invariants: for classes and systems as a whole, identify things that should always be true. For example, perhaps a field should never stray from certain values, or never be null.
- Pre-conditions: for each method, identify what must be true before the method is executed.
- Post-conditions: for each method, identify what must be true after the method is executed.
At the highest level, the invariants for the system (or subsystem) as a whole must be informally proven. This is done by examining each call from the top down, using the known post-conditions to prove the invariants have held. Invariants do not need to be true within the code of the method. Invariants can be temporarily suspended within methods, as long as they are true when the method exits.
Alternatively, you can start from the bottom of the call chain, and determine if the pre-conditions will be true for each call going up.
Ideally you do some informal proving of the call chains before you write the code.
Write code to assert (crash or throw exceptions if not true) the pre-conditions at the beginning of each method, and the post-conditions at the end of each method. If any of these things are too expensive to check at run-time, do it only in debug builds.
The end result is that your code will crash immediately, as soon as you start writing it. These are either bugs or specification errors. The crash may require a change to the invariants, pre-conditions, or post-conditions. But keep these facts as "tight" as possible; restrict the range of program states as much as possible.
You will find bugs as soon as you start executing code. You will still need tests in order to thoroughly exercise code.
Ideally we would all have automatic formal proof checking compilers, but that day seems to be still distant in the future.
If this seems like too much work, do it only for the low-level, most critical sections of code.