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.[1. And as legendary programmer Raymond Chen points out in a comment to this entry, 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.