Skip to main content

Runtime Errors

Example 1: Reading the stack trace of a runtime error

Here’s Palindromes.java. It’s a file that contains a main function for testing & printing, and an isPalindrome function that returns true when its input String is a palindrome.

Unfortunately, this program is buggy. Let’s try to find out what happens when we run it.

public class Palindromes {

    public static void main(String[] args) {
        String input = "hannah";
        if (isPalindrome(input)) {
            System.out.println(input + " is a palindrome.");
        } else {
            System.out.println(input + " is NOT a palindrome.");
        }
    }

    public static boolean isPalindrome(String word) {
        // base case - can't repeat the step of "peeling off first and last letters"
        // when there's only one letter, e.g.:
        // r a c e c a r
        // X X X ! X X X

        if (word.length() == 1) {
            return true;
        }

        // check if first and last are the same
        char firstChar = word.charAt(0);
        char lastChar = word.charAt(word.length());
        boolean firstAndLastMatch = firstChar == lastChar;

        //if they match, recurse on the string without the first and last letters
        return firstAndLastMatch && isPalindrome(word.substring(1, word.length() - 1));
    }
}

Compile the code

The code does compile. That’s a good sign, but it doesn’t mean that our code works, just that it will run.


Run the code

Here’s where the error pops up. Before we think too hard about solving it, we should make sure to note that this is a runtime error, or a bug in our code that causes an error sometime after the program is compiled and started.

Runtime errors usually happen when you try to perform some operation on some data that is outside of an acceptable range of values. An example would be trying to call Math.sqrt(-1), since we can’t represent the square root of a negative number as an int. This looks similar to a Type Error, but a Type Error would lead to a compilation error. You have the right data type in general, but the specific value that you’re using is invalid.

Our first step on seeing a runtime error like this should be to break apart the message that’s printed.


Understanding the Error Message

Here’s our error message.

Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 6
        at java.lang.String.charAt(String.java:658)
        at Palindromes.isPalindrome(Palindromes.java:24)
        at Palindromes.main(Palindromes.java:5)

How do we read it?

1. We start by examining the STACK TRACE.

The stack trace is the series of “at statements” printed at the bottom of your exception. It shows the sequence of functions that were called leading up to your error.

We start at the bottom, as this is the first function called. Here that’s main, which makes sense: main is always the entry point to Java programs. We see at the end of the line (Palindromes.java:5), meaning that main called another function on line 5. To see what that function is, we look to the next line up.

The stack trace has the first (outermost) function call at the bottom. Each line number in a line of the stack trace refers to the line where the next function was called. The next function is directly on top of the previous, and the last function to be called before the error crashed the program is at the top of the trace.

The next line up indicates that main calls isPalindrome. Following the same pattern, we find that isPalindrome calls another function on line 24. That function (from the next line up!) is charAt. This brings us to the top of the stack trace, and it means that we’ve found the function call that crashed the program: charAt. Moreover, we know that the specific call to charAt that crashed the program lives on line 24.

2. Next, we look to the name of the exception to tell us what actually went wrong.

Exceptions are how Java tells us that we hit a runtime error. We say that exceptions are “thrown” by Java when the underlying bug causes the program to crash.

The name of this exception is StringIndexOutOfBoundsException. Let’s break it apart to get some insight as to why this exception was thrown.

StringIndexOutOfBoundsException, like its array counterpart, is a remarkably common error. Recall that a String is essentially a sequence of chars (characters), and that we can index it with charAt to ask for the character at a particular position. Like all indexing in Java, indices start at 0, meaning that "Harry".charAt(0) == 'H', "Harry".charAt(4) == 'y', and trying to ask for an index smaller than 0 or larger than 4 would throw a StringIndexOutOfBoundsException.

If a String s has a length of n, i is a valid index for s.charAt(i) if 0 <= i < n.

3. Finally, let’s look at the Exception Description Java gives us.

After the name of the exception, Java prints the message: String index out of range: 6. There are two especially important components to this message.

4. Let’s summarize what we’ve learned just from the Exception.

We get a StringIndexOutOfBoundsException when we attempt to call charAt on line 24 inside isPalindrome. The index that causes this problem is 6, which suggests that the String we’re calling charAt on probably has a length of 6 or less.

Fixing the problem.

You might already have a sense of how to fix this problem having looked at the code, but it’s good to check your assumptions. To do this, we can use Print Statement Debugging carefully placed around the lines where the error occurs.

Print Statement Debugging: adding print statement(s) to your code to check the values of your data before the program crashes.

We’ve already narrowed it down that 6 is somehow too large of an index for the String it’s called on. Looking at line 24, where the exception occurs, we see:

char lastChar = word.charAt(word.length());

Why is 6 too large here? We can add a print statement on the line above to check what the String word actually contains, and check how long it is.

System.out.println(word + " has a length of " + word.length());
char lastChar = word.charAt(word.length());

This print statement shows us the value of word as well as its length. Now, running our program results in the following:

hannah has a length of 6
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 6
        at java.lang.String.charAt(String.java:658)
        at Palindromes.isPalindrome(Palindromes.java:25)
        at Palindromes.main(Palindromes.java:5)

We still have our same exception printed, but the first line shows the result of our print statement too. It turns out that right before the program crashes, word has the value "hannah", which is a String six characters long. Of course, then, word.charAt(6) would result in an error, since the index of the last character is 5 instead. Taking a close look at line 24 reveals that ask for word.charAt(word.length()), and indexing using the length of the same String will always be a problem. What I should have written instead is:

char lastChar = word.charAt(word.length() - 1);

This, of course, is the proper way to get the last character from a String. That character will always live at index word.length() - 1`. Now we can compile the code again and see what happens…

Example 2: Squash one bug, and you find another.

This should now be the state of our code.

public static void main(String[] args) {
        String input = "hannah";
        if (isPalindrome(input)) {
            System.out.println(input + " is a palindrome.");
        } else {
            System.out.println(input + " is NOT a palindrome.");
        }
    }

    public static boolean isPalindrome(String word) {
        // base case - can't repeat the step where there's only one letter
        // r a c e c a r
        // ^ ^ ^   ^ ^ ^
        if (word.length() == 1) {
            return true;
        }

        // check if first and last are the same
        char firstChar = word.charAt(0);
        char lastChar = word.charAt(word.length() - 1);
        boolean firstAndLastMatch = firstChar == lastChar;

        // if they match, recurse on the string without the first and last letters
        return firstAndLastMatch && isPalindrome(word.substring(1, word.length() - 1));
    }

Fortunately, this code still compiles. But since we hope that we’ve solved a bug, we’d also like it to run without crashing. When we run it now, we get the following:

Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 0
      at java.lang.String.charAt(String.java:658)
      at Palindromes.isPalindrome(Palindromes.java:23)
      at Palindromes.isPalindrome(Palindromes.java:28)
      at Palindromes.isPalindrome(Palindromes.java:28)
      at Palindromes.isPalindrome(Palindromes.java:28)
      at Palindromes.main(Palindromes.java:5)

It still crashes! But with a different message than before. We don’t have a correct program yet, but at least we have a new kind of problem. This is progress.

When you discover that your code has a bug, it’s usually the case that your code has several bugs hiding on top of each other. You will often solve one problem successfully, like we did, only to discover another underlying issue. Such is programming.

Let’s get to work.

Reading the Exception

1. We start by examining the STACK TRACE.

Like before, start with the stack trace from the bottom. Again we see that the first function call is from main on line 5. This call goes to isPalindrome, which calls the next function on line 28. Note that before our program crashed at line 24 of isPalindrome so we have even more evidence that we fixed the previous bug. The next function that isPalindrome calls is itself, isPalindrome. This is a signature sign of a recursive function: a call stack where a function appears repeatedly on top of calls to itself. This pattern repeats until one last call to isPalindrome, which then calls charAt on line 23. We have two main takeaways from this stack trace: first, we know that the error occurs when the fourth call to isPalindrome calls charAt; second, we take note that the error occurs only when we’re working with a String smaller than the original input "hannah" since each recursive call to isPalindrome operates on a shorter and shorter String.

2. Follow with a look at the EXCEPTION NAME.

The name is StringIndexOutOfBoundsException like before. The same conclusions apply:

3. EXCEPTION DESCRIPTION

The message that Java prints for us is String index out of range: 0. Like before, String index out of range means that we used an index that’s too small or too big. We used an index that was too big last time, but using a negative number is another obvious way to fail. That being said, Java reports that the bad index value is 0 here. This may strike you as strange, since indices for Strings start at 0. Thus, 0 should simply be the index of the first character of a String.

Using Print Statements to Debug

It might not be immediately clear why 0 could ever be an invalid index for a String. Let’s try using Print Statement Debugging to get a grasp of what’s happening. The statement that we used before should be a good template for this type of problem:

System.out.println(word + " has a length of " + word.length());

One issue that we need to address, though, is that we can’t simply put the statement in the same place as before. We remember from the stack trace that the error occurs at the line number 23, and our previous spot to test was just after this at line 24. This would mean that anything that gets printed would not reflect the state of the data right before the moment the bug occurs. So, we write the following:

System.out.println(word + " has a length of " + word.length());
char firstChar = word.charAt(0);

Compiling and running this program now gives us the following output:

hannah has a length of 6
anna has a length of 4
nn has a length of 2
 has a length of 0
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 0
        at java.lang.String.charAt(String.java:658)
        at Palindromes.isPalindrome(Palindromes.java:24)
        at Palindromes.isPalindrome(Palindromes.java:29)
        at Palindromes.isPalindrome(Palindromes.java:29)
        at Palindromes.isPalindrome(Palindromes.java:29)
        at Palindromes.main(Palindromes.java:5)

Like before, we have a bunch of printed lines before our program just ends up crashing anyway. This time, we see that this print statement gets called a total of four times, and this matches up quite well with our understanding of the program execution from the stack trace. We saw there four calls to isPalindrome, and this output shows us that each call ends up printing the input isPalindrome recieves along with its length in characters.

Now, looking at the list of printed outputs before the Exception message is printed: that last line looks weird. Why didn’t anything print before "has a length of 0"? Looking at the last recursion step, we had nn as our input. The recursive step takes the first and last characters off the String and compares them. Since they were the same, the program checks to see if what’s left is a palindrome. So what’s left… an empty String! It looks like this: "". There are no characters inside it, so the index 0 refers to the first character, but there is no first character because an empty string is empty! Aha!

We’re trying to use charAt() on an empty string, which just doesn’t work. So what do we do? Change our index math so that we don’t ask for something invalid? No, we update our base case. Here it is written currently:

if (word.length() == 1) {
    return true;
}

Looking at it, it becomes obvious that this is an inadequate base case. When we have an original input String that’s even in length, then none of the recursive steps will ever have a length of 1. Yet, an empty string with a length of 0 is vacuously a palindrome. There’s nothing there, and nothing backwards is still nothing. So we fix our base case to handle this type of input and move on!

if (word.length() <= 1) {
    return true;
}

Summary

We had a program with two similar bugs. We used the Exception messages from the crashes, along with cleverly placed print statements, to solve both bugs. The first was a simple mistake of index math, and the second revealed that our choice of base case was clearly not covering all possible outcomes. In both cases, we used information that Java provided us to quickly fix our errors.