1

Sometimes I ran into errors which are really hard to debug. So my meta question is the following:

  1. You have multiple chunks of code, let's say A -> B -> C -> D
  2. Your code don't crash but the result isn't what you expected
  3. You clearly don't know from where the error comes, so you tried to change all chunks one by one to find the bug. First A to a but crash the code so you undo your changes
  4. Then you try the same with B, C and D
  5. Each time you change the chunks it breaks your code
  6. When the stars are aligned, you changed two chunks at the same time (e.g. B and C) and it works...

So my question is: how can we avoid those situations?

With my little 5 years of programming experience I encounter only the case where I have to change two different part of code. (Just imagine if you have to fix more than 2 chunk of code to see the true result appear.)

I have never seen someone talked about this subject, but maybe I don't have the right words, so any suggestions to help me to gather more information about this subject are welcome.

2
  • 1
    Attached a debugger and step through the code, verifying the intended activity as you go along?
    – jleach
    Commented Mar 17, 2017 at 22:28
  • 3
    Reading and understanding the code helps. Stating what code should do first, then writing code that does what it should do, that helps a lot.
    – gnasher729
    Commented Mar 17, 2017 at 22:47

3 Answers 3

5

Your problem seems to be caused by the lack of understanding of your code. Best advice I can give you is learn how to read code (your own and written by others), so that you can see what's good and bad about it.

Practical advises:

  1. Write comprehensible code. Understanding what is and is not comprehensible usually comes when you learn how to read code, hence my advice above.
    1. Decompose your code into smaller parts which are easier to understand. Use descriptive names for everything.
    2. Use type system to your advantage. For example, if you're dealing with phone numbers, create a class PhoneNumber instead of passing around strings.
    3. Avoid mutation; avoid global or otherwise implicit state, whenever possible.
  2. Use debugger instead of changing stuff and seeing what happens. This method you're using is called shotgun debugging and it's known for low efficiency. Step-by-step debugging will help you better understand the most low-level parts of your code, where the most bugs hide.
  3. Write tests whenever you can for any non-trivial parts of your logic.
1
  • The tests are the most important part IMHO. They should flag the broken component A, B, C or D
    – marstato
    Commented Mar 20, 2017 at 18:07
2

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.

2

The existing answers are really good. I'd add 2 more points:

  1. Let the compiler do the work of debugging for you. Set its warning level to the highest level possible, and if you can, set warnings to be errors. Your code should be able to be compiled with no warnings whatsoever. This will reduce the number of small logic errors and typos that happen to compile.
  2. Use a static analyzer to tell you about larger harder-to-find logic errors. It can tell you things like that you're forgetting to free memory, or that you haven't initialized a variable when a certain condition occurs.

Also, there are plenty of debugging tools beyond just the debugger. Most systems have tools that can track memory allocations to help you find leaks or use-after-release issues. Tools like valgrind, address sanitizer, etc. Learn to use them!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.