5. Handling Errors

When writing programs, programmers inevitably make mistakes. Even professional programmers who write programs using the best techniques and practices occasionally produce flawed code. Created in God's image, we are nevertheless imperfect beings, and our limitations often negatively affect the programs that we write.

Sometimes the mistakes are very easy to find -- perhaps the compiler refuses to translate the program because of a spelling or punctuation error. Other mistakes are not so obvious, and the problem may remain undetected until a user triggers just the right set of conditions to cause the program to malfunction. Even more problematic are the mistakes that cause the program to produce incorrect, but believable, results.

Programmers have names for these mistakes -- they call them "bugs." Let me be very precise about what a bug is. A bug is a failure of a program to conform to its specification. The specification, sometimes called the requirements, is the document that describes what the program is supposed to do. A program may run to completion without crashing, but behave in some way that contradicts the specification. Whenever that happens, we have identified a bug.

Let's examine each of the kinds of problems that can occur in a program, how you find them, and what you do to fix them.

5.1. Syntax Errors

It would be nice if, having written your code, you could expect that it would work. Usually, no matter how careful you are when writing the code, a few syntax errors will creep in. A syntax error is a failure of the program to conform to the rules of the C# language, usually due to a spelling or punctuation error (for example, omitting a semicolon at the end of a statement, or mistyping a method name). You will find out that you have a syntax error when you invoke the compiler to translate your program to machine code. At that point, the C# compiler will reject your program with some kind of error message, and will refuse to produce machine code. Syntax errors are the easiest errors to detect, because the compiler tells you about them. They are also the easiest to fix, because the error message usually points you to the approximate place in the code where the problem is located. They prevent you from running your program, so there's no chance that one can "slip by" and cause problems later on.

Unfortunately, while a compiler will always detect syntax errors, it's not very good about telling you exactly what's wrong. Sometimes, it's not even good about telling you where the real error is. A spelling error or missing "{" on line 45 might cause the compiler to choke on line 105. You can avoid lots of errors by making sure that you really understand the syntax rules of the language and by following some basic programming guidelines. For example, I never type a "{" without typing the matching "}". Then I go back and fill in the statements between the braces. A missing or extra brace can be one of the hardest errors to find in a large program. Always, always indent your program nicely. If you change the program, change the indentation to match. It's worth the trouble. Use a consistent naming scheme, so you don't have to struggle to remember whether you called that variable interestrate or interestRate.

In general, when the compiler gives multiple error messages, don't try to fix the second error message from the compiler until you've fixed the first one. Once the compiler hits an error in your program, it can get confused, and the rest of the error messages might just be guesses.

Occasionally the compiler will display error messages labeled "warning," and will produce machine code that you can run. These messages are not errors (which prevent the translation of machine code), but reflect potential problems that you ignore at your peril.

Maybe the best advice is: Take the time to understand the error before you try to fix it. Programming is not an experimental science.

5.2. Runtime Errors

If the compiler translates your program, you can run it. That opens the door to another category of problem: runtime errors. A runtime error is a problem that causes your program to stop unexpectedly ("crash"). Sometimes runtime errors are caused by flaws in your program, like attempting to divide by 0. Other runtime errors are caused due to environmental problems, like network or disk failures. You'll know when a crash has occurred because the computer will display a standard dialog box or crash message, like this one:

Unhandled Exception: System.FormatException: Input string was not in a correct format.
   at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info)
   at Area2.Main():line 13

This error occurred when the user entered the text 'asdf' when the program was expecting an int response from the Convert.ToInt32 method call on line 13. Like syntax errors, runtime errors are cryptic to beginners. For now, the best thing you can do is to focus on two parts of the message:

  • The first line gives a summary of the problem that occurred

  • The last line tells you which line in your program was executing when the problem occurred (in this case, line 13).

With these two pieces of information in hand, you can begin your detective work to determine how to fix the problem.

The good news is that you're not likely to encounter many runtime errors in the first few programs you write. The most common runtime error you can expect for now is the one shown above, when you use the readInt method to get a number from the user, and the user types in a non-numeric response. Eventually you will learn to write code that detects and handles invalid user input without a crash, but for now, you can't do anything about it, so don't worry about it.

5.3. Logic Errors

If your program compiles and runs without crashing, you might be tempted to congratulate yourself on having produced a working program. But if your definition of a working program is that it "runs without crashing," think again. The reality is that when you do get to the stage of a so-called "working" program, it's often only working in the sense that it does something. Unfortunately, often what it does is not what you want it to do. That's when you have a logic error.

When your program compiles without error, you must test the program to make sure it works correctly. That means it must meet the requirements in the program specification. Your first job, then, is to review the specification to make sure your program does what the specification says it should do.

Often the specification will include one or more test cases. A test case consists of sample input, together with expected output. Those test cases are a good place to start. If you type in the inputs, and your program does not produce the expected output, you know you've found a logic error.

Remember that the goal is not to get the right output for the two sample inputs that the professor gave in class. The goal is a program that will work correctly for all reasonable inputs. Test your program on a wide variety of inputs. Try to find a set of inputs that will test the full range of functionality that you've coded into your program. As you begin writing larger programs, write them in stages and test each stage along the way. You might even have to write some extra code to do the testing -- for example to call a subroutine that you've just written. You don't want to be faced, if you can avoid it, with 500 newly written lines of code that have an error in there somewhere.

You won't know your program contains logic errors unless you 1) know what the correct output looks like for a given set of inputs and 2) you test the program and notice that the program's actual output doesn't match the expected results. Sometimes incorrect output is obvious -- negative numbers instead of positive numbers, for example. Other times, unless you take the trouble to work out by hand what the correct answer should be for a particular set of inputs, you won't really know whether the program produced the correct output. Professional software development organizations have a quality assurance department whose job is to create test cases and test the program to make sure the outputs match the expected results. But good programmers don't wait for the QA folks to find the logic errors; they make their own test cases and do their own testing. So should you.

The point of testing is to find bugs -- semantic errors that show up as incorrect behavior rather than as compilation errors. And the sad fact is that you will probably find them. You can minimize bugs by careful design and careful coding, but no one has found a way to avoid them altogether. Once you've detected a bug, it's time for debugging. You have to track down the cause of the bug in the program's source code and eliminate it. Debugging is a skill that, like other aspects of programming, requires practice to master. So don't be afraid of bugs. Learn from them.

One essential debugging skill is the ability to read source code -- the ability to put aside preconceptions about what you think it does and to follow it the way the computer does -- mechanically, step-by-step -- to see what it really does. This is hard. I can still remember the time I spent hours looking for a bug only to find that a line of code that I had looked at ten times had a "1" where it should have had an "i", or the time when I wrote a subroutine named WindowClosing which would have done exactly what I wanted except that the computer was looking for windowClosing (with a lower case "w"). Sometimes it can help to have someone who doesn't share your preconceptions look at your code.

Often, it's a problem just to find the part of the program that contains the error. Most programming environments come with a debugger, which is a program that can help you find bugs. Typically, your program can be run under the control of the debugger. The debugger allows you to set "breakpoints" in your program. A breakpoint is a point in the program where the debugger will pause the program so you can look at the values of the program's variables. The idea is to track down exactly when things start to go wrong during the program's execution. The debugger will also let you execute your program one line at a time, so that you can watch what happens in detail once you know the general area in the program where the bug is lurking.

A more traditional approach to debugging is to insert debugging statements into your program. These are WriteLine statements that print out information about the state of the program. Typically, a debugging statement would say something like Console.WriteLine("At start of while loop, N = " + N). You need to be able to tell where in your program the output is coming from, and you want to know the value of important variables. Sometimes, you will find that the computer isn't even getting to a part of the program that you think it should be executing. Remember that the goal is to find the first point in the program where the state is not what you expect it to be. That's where the bug is.

And finally, remember the golden rule of debugging: If you are absolutely sure that everything in your program is right, and if it still doesn't work, then one of the things that you are absolutely sure of is wrong.