General Debugging Procedures/Tips

  1. Before you start
  2. Types of Errors in Your Program
  3. Understanding Error Messages
  4. The Debugging Process

Before you start

Before you start debugging, it is really important that your code is formatted properly (e.g., open and close braces are lined up). It will make locating errors much faster. Dr. Java includes a primitive Java Code Beautifier (a program that formats your code automatically). To use this utility, press Ctrl-A to highlight your entire code block, then press Tab. If you do not think the utility does an adequate job, you can search for a more sophisticated beautifier online. A good one to try would be Jacobe. To improve readability, it is also recommended to include some white space between blocks of code.

The Java Compiler outputs lines that contain run time errors. This is really helpful when debugging, as we do not have to look at all lines of code, instead focusing on a smaller block. By default, Dr. Java does not display line numbers. To turn on this option (and see the line that is generating the run time error), click on the Edit menu, and the Preferences option. Then, click on Display Options (second item on the left hand side), put a check mark for "Show all line numbers", and press OK. Below is a picture of what you should see when toggling on this option:

Types of Errors in Your Program

There are three basic types of errors that can exist in your program. They are:

a) Compilation Error

This type of error occurs when you do not follow Java syntax (rules and guidelines that all Java code must abide to). If there is a compilation error, the Java compiler will display an error message during compilation (the process of turning code into machine language). The program is never executed if such error existed.

b) Run-Time Error

This type of error occurs during the execution of your program. The program compiled without any problems. However, during execution, it encounters an error and stops executing at a certain line. The compiler will indicate which line of code is causing the problems.

c) Logic Error

The program compiles and executes without a problem. However, it does not do what is intended.

Understanding Error Messages

It is important that you can interpret error messages that the compiler displays. The message provides many useful clues to the problem at hand. Hence, you can debug your program much more efficiently if you understand its meaning. It is displayed in the Compiler Output pane (for a compilation error) or in the Interactions pane (for a run time error). Compilation errors are relatively easy to understand. Below is a screenshot of what they typically look like:

Sometimes, the compiler is not smart enough to pick out the exact line that needs to be fixed. However, with the help of this guide and experience, compilations errors should be easy to locate and fix. The most common errors are:

1) The compiler cannot recognize a symbol (usually variable name or method name). The error message displayed will have the following pattern:

Error: cannot find symbol
symbol: method{method name}(parameters)

or:

Error: cannot find symbol
symbol: variable {variable name}

2) The compiler thinks that bits of code are out of order, missing or at the wrong place. The error message displayed will have this pattern: Error: '{symbol}' expected

On the other hand, run time error messages are harder to decode:

The first line indicates the type of error encountered. In this case, it involves mismatch of input types. Lines after the initial message specify where the error is coming from. They are listed in a reverse order of execution, that is, the top line is the last line of code executed. In the above example, test2 calls the main method in test1. Then, the main method executes a method in the Scanner class (nextInt). In turn, nextInt executes the next method in the Scanner class, which threw the error message. Notice that all the lines follow the similar pattern of:
at {class name}.{method name}(source/line number)

You only have to worry about error messages originating from the classes you wrote, on the indicated lines. For example, you can ignore errors coming from java.util or sun.reflect.

The Debugging Process

Recognizing Bugs

You should be able to easily determine whether a bug exists in your program. If your program gives an error message, or does not output/perform what is expected, then there is a bug in your program.

By using different test cases, you can ensure that your program performs correctly under all conditions. You will learn more methodical testing approaches in another computer science course (CS134).

Isolating Bugs

Perhaps the most challenging step in the debugging process is isolating the location of the problem.

Isolating Compilation Errors:

As mentioned previously, the Java compiler does a relatively good job at isolating the source. If you do not think the highlighted line is the source of the problem, look at all the lines within the same scope. If you still cannot locate the bug, continue the search in the scope that is within the current one, if it exists. If not, proceed to the immediate outer scope that encompasses the scope of the highlighted line. For example, suppose we have:

	public static void main(String[] args)
  	{  int i = 5;
     	   for ( int p = 0; p < 5; p++ )
           { While ( i < p )
             { i ++;
             }
           }
        }

The compiler will highlight the { i++; line, and gives out the following error message: Error: ';' expected. So, the first line you would examine is the highlighted line – you do not see a mistake there. Next, look at lines in the current scope. Since i++; is the only line in the current scope, move on to the scope that is within the current one. Because such scope does not exist, you continue the search in the scope that encompasses the current one. This scope has the only one line: While ( i < p ) . You isolate the error by realizing that the keyword while is wrongfully capitalized.

Isolating Run Time Errors:

The Java compiler does a very accurate job of indicating lines that are causing the error. Hence, it is very easy to locate the source of the problem. Please refer to the Understand Error Messages section for more details.

Isolating Logic Errors:

Logic errors can be much harder to isolate because there can be many causes. Often, it is helpful to determine the values certain variables hold during different points of a program’s execution. If the variables are not holding the correct value, we can then track down the cause.

One way of doing so is using the Dr. Java’s Debugger. Since you will encounter this technique during the lab, this guide will focus on a different approach: using System.out.println to print values on to the screen. This approach is a little bit faster then using the Debugger. However, you do not have as much control (since the Debugger allows you to run the program one line or one loop iteration at a time, whereas System.out.println would run your entire program at once).

What variables should you use println to display the value of? There is not a set rule, but good targets include counters in nested loops, variables in compound Boolean expressions or complex operations, and any other variables that are constantly changing throughout the program. Where should you place the println statements? Think of the general path a certain variable goes through in your program. It is good idea to place a println statement on lines that changes the value of that variable.

Consider this problem:

Suppose integers 1 to 16 are stored in a 2d array:

[ 1 ] [ 2 ] [ 3 ] [ 4 ] 
[ 5 ] [ 6 ] [ 7 ] [ 8 ]
[ 9 ] [10 ] [11 ] [12 ]
[13 ] [14 ] [15 ] [16 ]

In another 2d array of Strings, store either "Yes" if the corresponding element in the 2d integers array is divisible by 2 and 3. Store the String "No" otherwise. Print out the results in a format of

yes yes no no
yes yes no no 

Suppose this is the solution we have:

     String[][] info = new String[4][4];
     int num = 1; //this is the current number we are examining, num will hold the value 1 to 16
     for (int i = 0; i < info.length; i++)
     { for (int j = 0; j < info[0].length; j++)
        { if ( num % 2 == 0  &&  num % 3 == 0)
          { info[i][j] = "Yes";
          }
          else
          { info[i][j] =  "No";
          }
        }
        num++;
     }
     //output
     for (int i = 0; i < info.length; i++)
     { for (int j = 0; j < info[0].length; j++)
        { System.out.print(info[i][j]);
        }
        System.out.println();
     }

When running this program, we get the following output:

No No No No 
No No No No 
No No No No 
No No No No  

Obviously, the result is incorrect, since it implies that all integers from one to sixteen are not divisible by both two and three. It might be difficult to trace the program by hand, since we have nested for loops. Instead, we can use System.out.println to display variable values at different points of execution.

What variables should we keep track of? The most important variable here is num, since we use it to determine if an element in the 2d integers array is divisible by two and three. Therefore, num is a good variable to keep track of. At what stage are we interested in the value of num? It would be useless if we put the println right after declaring num, since its value has not changed yet. We probably want to know its value during each iteration of the for loop, where num is incremented. Hence, we should place the println either before or after the num++ line. Suppose we add a println after the num++ line:

num++;
System.out.println("num has the value "+num);

When we run our program again, we get the following output:

num has the value 2
num has the value 3
num has the value 4
num has the value 5
No No No No 
No No No No 
No No No No 
No No No No

We can see right away that there is a problem with num. It was suppose to take on values from one to sixteen. Instead, it takes on values two to five only. Since num is only changed four times (as evident by the four lines of output), we realize that the nested for loops are not incrementing num the correct number of iterations. However, the problem does not appear to be the for loops themselves. Having a closer look, we discover that num++ is placed in the wrong scope: it should be placed in the body of the most inner for loop. A correct solution would be:

     String[][] info = new String[4][4];
     int num = 1;
     for (int i = 0; i < info.length; i++)
     { for (int j = 0; j < info[0].length; j++)
        { if ( num % 2 == 0  &&  num % 3 == 0)
          { info[i][j] = "Yes";
          }
          else
          { info[i][j] =  "No";
          }
          num++;
        }       
     }
     
     for (int i = 0; i < info.length; i++)
     { for (int j = 0; j < info[0].length; j++)
        { System.out.print(info[i][j]+" ");
        }
        System.out.println();
     }

Now, the new output is:

No No No No 
No Yes No No 
No No No Yes 
No No No No  

This indicates that six and twelve are divisible by both two and three, which is correct.

There is not a fool-proof method to isolating logic errors. With practice, you can become a skilled debugger.Below are some other tips that may help in the process:

Eliminating Bugs

Once you have identified and isolated the bug, the next step is fixing it. Since you now know the cause, resolving the issue should not be difficult. This guide contains various solutions to errors that you often encounter, so this section will not cover any specific problems. However, if you have a logic error, you may need to modify your program significantly. Here are some general tips in that situation: