(Este artigo está disponível em português.)
One of the most frequent categories of bad questions I see on StackOverflow is:
I wrote this program for my assignment and it doesn’t work.
[20 lines of code].
And… that’s it.
If you’re reading this, odds are good it’s because I or someone else linked here from your StackOverflow question shortly before it was closed and deleted. (If you’re reading this and you’re not in that position, consider leaving your favourite tips for debugging small programs in the comments.)
StackOverflow is a question-and-answer site for specific questions about actual code; “I wrote some buggy code that I can’t fix” is not a question, it’s a story, and not even an interesting story. “Why does subtracting one from zero produce a number that is larger than zero, causing my comparison against zero on line 12 to incorrectly become true?” is a specific question about actual code.
So you’re asking the Internet to debug a broken program that you wrote. You’ve probably never been taught how to debug a small program, because let me tell you, what you’re doing now is not an efficient way to get that problem solved. Today is a good day to learn how to debug things for yourself, because StackOverflow is not about to debug your programs for you.
I’m going to assume that your program actually compiles but its action is wrong, and that moreover, you have a test case that shows that it is wrong. Here’s how to find the bug.
First, turn on all compiler warnings. There is no reason why a 20 line program should produce even a single warning. Warnings are the compiler telling you “this program compiles but does not do what you think it does”, and since that is precisely the situation you are in, it behooves you to pay attention to those warnings.
Read them very carefully. If you don’t understand why a warning is being produced, that’s a good question for StackOverflow because it is a specific question about actual code. Be sure to post the exact text of the warning, the exact code that produces it, and the exact version of the compiler you’re using.
If your program still has a bug, obtain a rubber duck. Or if a rubber duck is unavailable, get another computer science undergraduate, it’s much the same. Explain to the duck using simple words why each line of each method in your program is obviously correct. At some point you will be unable to do so, either because you don’t understand the method you wrote, or because it’s wrong, or both. Concentrate your efforts on that method; that’s probably where the bug is. Seriously, rubber duck debugging works. And as legendary programmer Raymond Chen points out in a comment below, if you can’t explain to the duck why you’re executing a particular statement, maybe that’s because you started programming before you had a plan of attack.
Once your program compiles cleanly and the duck doesn’t raise any major objections, if there’s still a bug then see if you can break your code up into smaller methods, each of which does exactly one logical operation. A common error amongst all programmers, not just beginners, is to make methods that try to do multiple things and do them poorly. Smaller methods are easier to understand and therefore easier for both you and the duck to see the bugs.
While you’re refactoring your methods into smaller methods, take a minute to write a technical specification for each method. Even if it is just a sentence or two, having a specification helps. The technical specification describes what the method does, what legal inputs are, what expected outputs are, what error cases are, and so on. Often by writing a specification you’ll realize that you forgot to handle a particular case in a method, and that’s the bug.
If you’ve still got a bug then first double check that your specifications contain all the preconditions and postconditions of every method. A precondition is a thing that has to be true before a method body can work correctly. A postcondition is a thing that has to be true when a method has completed its work. For example, a precondition might be “this argument is a valid non-null pointer” or “the linked list passed in has at least two nodes”, or “this argument is a positive integer”, or whatever. A postcondition might be “the linked list has exactly one fewer item in it than it had on entry”, or “a certain portion of the array is now sorted”, or whatever. A method that has a precondition violated indicates a bug in the caller. A method that has a postcondition violated even when all its preconditions are met indicates a bug in the method. Often by stating your preconditions and postconditions, again, you’ll notice a case that you forgot in the method.
If you’ve still got a bug then learn how to write assertions that verify your preconditions and postconditions. An assertion is like a comment that tells you when a condition is violated; a violated condition is almost always a bug. In C# you can say
using System.Diagnostics; at the top of your program and then
Debug.Assert(value != null); or whatever. Every language has a mechanism for assertions; get someone to teach you how to use them in your language. Put the precondition assertions at the top of the method body and the postconditions before the method returns. (Note that this is easiest to do if every method has a single point of return.) Now when you run your program, if an assertion fires you will be alerted to the nature of the problem, and it won’t be so hard to debug.
Now write test cases for each method that verify that it is behaving correctly. Test each part independently until you have confidence in it. Test a lot of simple cases; if your method sorts lists, try the empty list, a list with one item, two items, three items that are all the same, three items that are in backwards order, and a few long lists. Odds are good that your bug will show up in a simple case, which makes it easier to analyze.
Finally, if your program still has a bug, write down on a piece of paper the exact action you expect the program to take on every line of the program for the broken case. Your program is only twenty lines long. You should be able to write down everything that it does. Now step through the code using a debugger, examining every variable at every step of the way, and line for line verify what the program does against your list. If it does anything that’s not on your list then either your list has a mistake, in which case you didn’t understand what the program does, or your program has a mistake, in which case you coded it wrong. Fix the thing that is wrong. If you don’t know how to fix it, at least now you have a specific technical question you can ask on StackOverflow! Either way, iterate on this process until the description of the proper execution of the program and the actual execution of the program match.
While you are running the code in the debugger I encourage you to listen to small doubts. Most programmers have a natural bias to believe their program works as expected, but you are debugging it because that assumption is wrong! Very often I’ve been debugging a problem and seen out of the corner of my eye the little highlight show up in Visual Studio that means “a memory location was just modified”, and I know that memory location has nothing to do with my problem. So then why was it modified? Don’t ignore those nagging doubts; study the odd behaviour until you understand why it is either correct or incorrect.
If this sounds like a lot of work, that’s because it is. If you can’t do these techniques on twenty line programs that you wrote yourself you are unlikely to be able to use them on two million line programs written by someone else, but that’s the problem that developers in industry have to solve every day. Start practicing!
And the next time you write an assignment, write the specification, test cases, preconditions, postconditions and assertions for a method before you write the body of the method! You are much less likely to have a bug, and if you do have a bug, you are much more likely to be able to find it quickly.
This methodology will not find every bug in every program, but it is highly effective for the sort of short programs that beginner programmers are assigned as homework. These techniques then scale up to finding bugs in non-trivial programs.
Pingback: arrays - Trovare i valori duplicati in un array in java
Pingback: dos – Alternative to dir command to query a directory in C – ThrowExceptions
Thank you for the information.
Thanks for the info!
Excellent summary; the sooner someone learns and uses those steps, the more hours/days/months will be saved. I know, as I’ve wasted a lot of days over my career due to not taking the time to do those steps.
One thing that can greatly improve success is to write test cases before you even start to code. That has saved me a lot, and cost me a lot when I don’t!
Debugging a broken thing is Hard, since each iteration may report “it is still broken” and you don’t learn much from that. It is much easier to learn by iterating on good code till you break it.
Consider stripping the misbehaving program down to something very trivial that “works”. Then keep adding little pieces of your proposed solution back into it, one tiny piece at a time. The instant it stops working, stop, as you have found a piece which is not correct, it should not be part of the solution. Learn from that, and propose a better solution.
While your approach works great for new code, it’s often less effective for debugging very large code-bases. In that case, I’ve found that you can do the opposite: Binary Search for Bug. In this approach, you delete/disable half of the code that is currently under investigation and see if the bug is still there. If so, you know that the half you did not delete has the bug.
I’ll note that Eric Lippert’s guide is highly compatible with Binary Search for Bug: breaking up your code into smaller methods and testing them independently is a great way to narrow down the location of a bug.
Binary search is certainly worthy of adding to Eric’s guide.
In the FPGA/ASIC/Digital hardware design, where we are using programming languages (VHDL and Verilog) to design hardware, it is not unusual for a tool to not support the latest language revision and result in an odd error message or crash. Sometimes a binary search is the only way to find the root cause – especially for a quirky tool crash.
Also I’ll note that the post is called “how to debug small programs”, not “how to debug large programs”.
How do you turn on all compiler warnings in python/spyder?
What does ‘turn on all compiler warnings’ mean? I am pretty new to this.
Python warnings are enabled by default, so you don’t need to turn on anything. You can read more about them on https://docs.python.org/3/library/warnings.html
You wrote a piece of code. It compiled successfully. Seems good. Then you parse parameters/files/signals… to it, it doesn’t output expected result ? Well, my all-time solution is just LOG IT. LOG EVERYTHING YOU CAN LOG.
A python code on machine learning ? Print all variables’ status to the console.
Working on a server ? Create a log file and write down everything.
C# app ? Open a textbox and put all your secrets there. Don’t worry, your boss would later delete it for you if you forgot about it.
Suppose you were a bug. Where would you hide? Normally I know two places: the part of code that is rarely visited (special combination of parameters) or code that seems to be added when most was working (function name not covering all the things done or very detailed actions within code with mostly simple lines).