Run-time Errors

Once your code compiles, you will often get "run-time" errors. These errors (also called "exceptions") halt the execution of your program. There are fewer possible run-time errors than there are compile-time errors, but the procedure for debugging them is not always clear. This page suggests a general method of debugging any run-time errors you encounter.

Important Points to Keep in Mind

Debugging run-time errors in your code can be an extremely time-consuming process. However, there are a few points that you should keep in mind when attempting to determine where the problem is in your code.

  1. Java executes code in a strict order.

    The only time the flow of code execution will "jump" to another place is when you instruct it to do so. A loop, an if statement, or calling another method will change cause "out of order" execution in that Java will execute something other than the next statement in top-to-bottom order.

    For example, consider the following code:

    01  numPeople++;
    02  numRegistered++;
    03  int age = person.getAge();
    04  if( age >= 16 ) {
    05      person.setDrive(true);
    06  } else {
    07      person.setDrive(false);
    08  }
    09  ...
    

    The important thing to recognize here is that Java will execute lines 1 through 3 in that order, but will not execute line 4 directly after line 3. Rather, because line 3 calls another method: Java will execute the first line of the getAge() method immediately upon reaching line 3 in the above code. When the getAge() method finishes, then execution will be returned to the code shown above: execution of line 3 will be completed, storing the value rturned by setDrive(...) into the variable age, and execution will continue on to line 4.

    Similarly, when Java evaluates the if clause at line 4, either line 5 or line 7 will be executed next, followed by line 9. Understanding the order in which lines of code are executed is crucial to understanding what a program does, and hence to debugging a program that doesn't do what it should.

  2. A single line of code stopped the execution of your program.

    When a run-time error occurs, DrJava will tell you the line that was executing when the program crashed. That line, and only that line, is responsible for causing the run-time error to occur.

  3. But ... the line containing the error may not need changing.

    Although the execution of one specific line is what caused your program to stop running, this line is not necessarily incorrect. For example, say we get a NullPointerException on the following line in our code:

    Coordinate c = room.getClick();
    

    Although this line caused the program to halt, it's actually correct - this is how you get a board position. The problem is that the variable room has, elsewhere in the program, been set to null. In other words, the specific line that is the cause of the error may not necessarily be the reason for the error; code elsewhere in your program may need fixing instead.

  4. Values at runtime aren't always what you think they are.

    Making assumptions about which values certain variables hold during the execution of your program can lead to several problems. For instance, consider the following method:

    01  public double getCourseAverage()
    02  {   
    03      int total = 0;
    04      for( int i=0; i<grades.length; i++ )
    05      {
    06          total += grades[i];
    07      }
    08
    09      int n = getNumberOfStudents();
    10      double avg = total / n;
    11     
    12      return avg;
    13  }
    

    When we look at this method, we expect that it will work because we assume that grades holds all of the students' correct marks and we also assume that the method getNumberOfStudents() will correctly return the number of students in the course. However, upon running the program, we may find that the getNumberOfStudents() method returns 0, causing a "Division by Zero" error. Or we may find that the grades array has not been filled with values, has been filled incorrectly, or has a length greater than what we expected.

    In short, don't assume anything about your code during runtime, no matter how trivial. Often, the thing you don't expect may actually be the reason for the error. In fact, that's usually the case - if you had expected it, you would have written different code!

The next section outlines steps you should take in order to find bugs in your code that are the source of run-time errors.

How to Debug Your Code

Reading the previous section, it would seem that there may be errors anywhere in your code, even where you expect them the least, and that they are very hard to find. This is partially true: errors can be in many different places and can take a lot of time to fix, but fortunately there is a very logical way of going about doing this.

Keeping the above facts in mind, the following lays out a series of three important steps you should take every time you get a run-time error. Following these steps, you will always be able to find the bugs in your code (even though some may take a lot longer than others).

  1. Look at the line number and type of error.

    As mentioned in the previous section, one line is going to be the reason why your program stopped running. Different development environments (e.g. Dr. Java, JBuilder, JCreator) will most likely report errors in a slightly different format, but the the error messages you receive in the console output will all contain the same basic information:

    You should pay special attention to this information and use it as a starting point for debugging your code. The following example shows a sample error message.

    Code What shows up in the interaction pane

    This code's main method is executed.

    Upon execution, we receive this message in the interaction pane.

    Line (b) tells us the type of the error: a null pointer exception.

    Then line (c) tells us which method of which class caused the error, along with the line number of the statement within that method that caused the error - in this case, line 26 in the method insideTheRoom() of the BoardGoneMissing class is where the error occurred.

    Line (d) tells us that line 20 in the file BoardGoneMissing.java contains the statement from which the method shown in line (c) was called.

    And so on, all the way back to main( String args ).

    (Lines (g) through (j) tell us what methods were called by the Java runtime system in reaching execution of main(...), which are of no use to us.)

    In this case, it's easy to spot the assignment of null to room that resulted in the exception. In real life, it may be more difficult - room might have been set to null by a method called from outsideTheRoom() before calling insideTheRoom(..), for example.

    01: public class BoardGoneMissing {
    02:     
    03:     public static void main( String[] args ) 
    04:     { 
    05:         BoardGoneMissing program = new BoardGoneMissing();
    06:         program.run();
    07:     }
    08:     
    09:     public void run() {
    10:         // ...omitted code...
    11:         Board room = new Board(12,12);        
    12:         // ...omitted code, that doesn't mention room
    13:         outsideTheRoom( room );
    14:     }
    15:     
    16:     public void outsideTheRoom( Board room ) {
    17:         // ...omitted code...
    18:         room = null;
    19:         // ...more omitted code
    20:         double howFarFromHome = insideTheRoom( room );
    21:     }
    22:       
    23:     public double insideTheRoom( Board room ) 
    24:     { 
    25:         room = null;
    26:         Coordinate c = room.getClick();
    27:         return Math.sqrt(   c.getRow()*c.getRow() 
    28:                           + c.getCol()*c.getCol() );
    29:     }
    30:  
    31: }
    
    (a) > java BoardGoneMissing
    (b) NullPointerException: 
    (c)   at BoardGoneMissing.insideTheRoom(BoardGoneMissing.java:26)
    (d)   at BoardGoneMissing.outsideTheRoom(BoardGoneMissing.java:20)
    (e)   at BoardGoneMissing.run(BoardGoneMissing.java:13)
    (f)   at BoardGoneMissing.main(BoardGoneMissing.java:6)
    (g)   at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    (h)   at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    (i)   at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    (j)   at java.lang.reflect.Method.invoke(Method.java:585)
    (k) >
    

    Using this information, we can "trace through" the order of execution in our code, which will lead us to the error.

  2. Think critically about your code.

    Using the error message we receive from the console, start at the line which caused the error and trace backwards through the method calls that reached that line, asking yourself questions about your code as you go along. As a simple example, let's use the code example in step 1:

    After creating another board object and assigning it to room, you should re-compile and run your code again to test whether you've actually fixed the bug. And of course you may run into other errrs...

    Certainly, the types of questions you ask yourself will change with your programming experience. This works for some errors, but not for all of them. If your code is too complicated for you to just be able to "see" the problem (and this is usually the case), then the next step often helps.

  3. If the solution is not obvious, systematically isolate the problem - interactive source level debugging

    DrJava, like most Interactive Developent environments (IDEs), allows you to step through your code one statement at a time. That is, after each statement is executed, execution is suspended until you click a button. This enables you to follow the sequence of statements actually executed, which may not be what you expected ... which may be your problem. In DrJava, you accomplish tracing by

    1. first entering Debug Mode via the Debugger > Debug Mode menu item;
    2. placing a "break point" on an executable statement at the beginning of your program -- e.g. line 06, containing the statement program.run() of our first example program -- via the Debugger > Toggle Breakpoint on Current Line menu item;
    3. clicking the Run button, which will cause execution of your program to begin, but pause when it reaches the statement on which you have set a breakpoint;
    4. clicking either the Step Into or Step Over buttons to execute the next statement. (These two buttons are equivalent if the statement on which execution is paused does not contain a method call. If it does contain a method call, then Step Into causes DrJava to call the method and pause on its first executable statement, whereas Step Over causes DrJava not to enter the method and pause, but rather to completely execute the current statement and pause before executing the next statement in the same method, thus "stepping over" the method call.

    Of course, if you already partially localized your bug, you can put your initial breakpoint at the beginning of the code you believe contains your bug, and start debugging from there.

    While you're in debug mode, DrJava can "watch" variables for you: type a variable name in the "name" cell on the first unused line of the Watch pane, and DrJava will display its value for you. Even better, DrJava will update those values for you automatically as you step from line to line via the Step Into or Step Over buttons.

    As you do this, the Stack pane will show you the chain of method calls by which the program reached the current line of execution from main(...).

    Moreover, while the program is paused, you can type commands into the interaction pane and they'll be executed. You can call methods, such as those that return a value from inside an object visible from the statement at which execution has been suspended.

    More information on using Debug Mode in DrJava can be found in DrJava's Help menu.

  4. Another way to isolate the problem - System.out.println(...)

    Another approach to isolating the problem is use System.out.println() to print out the value of variables as your program executes.

    The example below is identical to the one from step 1, except I have included output to the console (in bold).

    Code Console Output

    We execute this code's main method and have included some messages to the console along the way.

    The console output from running this code. We should take note of the following things:

    • Lines (b), (c), and (d) appearing in the console window mean that lines 7, 14, and 15 were executed, and in that order.
    • Similarly, because of the lack of corresponding output to the console, we know that lines 17 and 9 were not executed.
    • The output of line (d), which was from line 15 in our code, tells us that p was either never initialized (the case here) or was set to null before method Test(...) was called. If p did contain a Person reference (i.e. it was non-null), then the output to the console would instead be something like p = Person@5e17f4, meaning that p refers to a Person object located at memory address 5e17f4."
    01 public class Test
    02 {
    03   private Person p;
    04   
    05   public static void main( String[] args )
    06   {
    07     System.out.println("Creating a Test object"); 
    08     Test t = new Test();
    09     System.out.println("Done program"); 
    10   }
    11   
    12   public Test()
    13   {
    14     System.out.println("Inside Test constructor"); 
    15     System.out.println("p = " + p); 
    16     String personName = p.getName();
    17     System.out.println("personName = " + personName); 
    18     int nameLength = personName.length();
    19     System.out.println(nameLength);
    20   }
    21 }
    
    (a) > java Test
    (b) Creating a Test object
    (c) Inside Test constructor
    (d) p = null
    (e) NullPointerException: 
    (f)   at Test.(Test.java:16)
    (g)   at Test.main(Test.java:8)
    (h)   at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    ...
    >
    
    

    As you can see, printing out our own messages to the console would have revealed the source of the problem to us immediately. For more complex and involved code with several different classes/methods interacting with one another, visually displaying this information is very useful.

    In general, consider including System.out.println() statements in the following helpful places:

    1. Just before the line which causes the error in order to look at the values of any variables or the return values of any method calls.
    2. At the beginnings of methods to check whether or not they are being called (e.g. System.out.println("Method xxx of class yyy called.")).
    3. At the ends of methods/code blocks to ensure that the execution makes it all the way through (e.g. System.out.println("Method xxx of class yyy completed.")).
    4. Inside of loops and if statements to ensure that they are being entered (or are repeating an appropriate number of times).
    5. After initializing variables to ensure they are initialized to the correct values.
    6. Anywhere else you feel would be helpful in the context of the error (e.g. never be afraid to include more output than necessary).

    A good general rule is "never delete these System.out.println(...) statements." Instead, declare a boolean variable - eg debug - and execute the print statements only when the value of that boolean is true. In this way you can turn debugging output on and off as needed.

    public class Something {
        private boolean debug = true;
        ...
        public int someMethod(...) {
            ...
            if( debug ) System.out.println(...);
    	    ...
        }
        ...
    }
  5. Form and test hypotheses

    Difficult bugs require methodical analysis and testing.


Created by Terry Anderson (tanderso at uwaterloo dot ca) and adapted for CS 133 by JCBeatty. Last modified on 24 Sep 2006.