Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Checked and unchecked exceptions

In this lesson, we'll learn about Exceptions that are "checked" or "unchecked" by the compiler and, more broadly, about the Exception hierarchy in Java.

Recap

Here's one version of the code we left off with in the previous lesson. We're reading a file, and accounting for the case where the file doesn't exist.

double getTotalMilesRun(String fileName) {
  if (fileName == null || fileName.isEmpty()) {
    throw new IllegalArgumentException("fileName cannot be null or empty");
  }

  double totalMiles = 0.0;

  try {
    // Create a Scanner to read the file
    Scanner fileScanner = new Scanner(new File(fileName));

    // Read each line of the file
    while (fileScanner.hasNext()) {
      String line = fileScanner.nextLine();
      String[] parts = line.split(",");
      String name = parts[0];
      double miles = Double.parseDouble(parts[1]);
      totalMiles += miles;
    }

    // Always a good idea to close the Scanner when we're done with it.
    fileScanner.close();
  } catch (FileNotFoundException fnfe) {
    // Gracefully handle the exception
    System.out.println("Could not find a file called " + fileName);
    System.out.println("Error message: " + fnfe.getMessage());
  }

  return totalMiles;
}

We were forced to account for that case by the compiler, because the Scanner constructor declares that it throws a FileNotFoundException.

That's because the FileNotFoundException is a checked exception. The next section discusses the differences between checked and unchecked exceptions and shows some examples.

The "Throwable" type hierarchy

We saw a brief segment of the Exception type hierarchy in the previous lesson. Here's a fuller picture, though still not exhaustive.

The top-level Throwable type is the root of the exception/error hierarchy in Java. Its two main subtypes are the Error type and the Exception type.

Error is used for things like StackOverflowError (thrown when your program runs out of stack space, usually due to infinite recursion) and OutOfMemoryError (throw when, well, when your program is out of memory). These are usually problems at the JVM level, and you typically don't try to catch or handle them in your code.

We've been mainly concerned with the Exception type and its subclasses.

flowchart TD
  Throwable --> Error
  Error --> OutOfMemoryError
  Error --> StackOverflowError
  Throwable --> Exception
  Exception --> IOException
  Exception --> Others...
  IOException --> FileNotFoundException
  Exception --> RuntimeException
  RuntimeException --> NullPointerException
  RuntimeException --> IllegalArgumentException
  RuntimeException --> NumberFormatException

  classDef runtimeEx fill:#ffcccc,stroke:#ff0000,stroke-width:2px,stroke-dasharray: 5 5,color: #000000
  classDef checkedEx fill:#ccffff,stroke:#0000ff,stroke-width:2px,color: #000000

  class RuntimeException,NullPointerException,IllegalArgumentException,NumberFormatException runtimeEx
  class Exception,IOException,FileNotFoundException,Others... checkedEx

Broadly speaking, exception types that are subclasses of Exception but not subclasses of RuntimeException are checked by the compiler. These are represented as blueish nodes in the type hierarchy above.

Checked exceptions are the ones for which the compiler checks whether or not you have a plan to handle them (i.e., a try-catch or a throws).

On the other hand, subclasses of RuntimeExceptions are unchecked by the compiler. These are represented as reddish nodes in the type hierarchy above.

Unchecked exceptions are the ones for which the compiler does not require exception-handling.

For example, before dereferencing an object pointer (i.e., before using the dot operator . on the object to access its methods or instance variables), you need to be sure that it's not null. If it is null, you would get a NullPointerException, which is an unchecked exception.

In the previous lesson, we saw an example of checking for null-ness and throwing an IllegalArgumentException in the case of invalid. IllegalArgumentException is another example of an unchecked exception, commonly used when preconditions are violated (e.g., when a method is called with invalid inputs or "arguments").

Should you throw checked or unchecked exceptions?

When you're writing a method and want to notify the method caller's about some erroneous conditions, you have a choice: you can throw a checked exception, or you can throw an unchecked exception. Which one should you use?

The conventional wisdom is (courtesy of Joshua Bloch):

  • Throw checked exceptions for cases where the error is recoverable. That is, your method will force the calling method to handle the exception. This is useful for things like reading files, where the caller can do something about the error (e.g., ask the user for a different file name, or create a new file).
  • Throw unchecked exceptions for things that are likely programmer errors, like passing null to a method that asks for an object, or passing a negative number to a method that asks for positive number. These are cases that simply shouldn't happen, and the programmer who gets hit with an unchecked exception should be able to fix the problem by changing their code, rather than you having to write code to handle the exception.1

Handling potential unchecked exceptions

So, if the compiler doesn't require you to handle unchecked exceptions, does that mean you can ignore them? Absolutely not!

Unchecked exceptions can still crash your program if they "escape containment", so you need to either make sure they won't occur (e.g., because you've checked preconditions), or you need to handle them with a try-catch block if they do occur.

How do we know which unchecked Exceptions might occur in our code? Some good practices are:

  • Be defensive about any data you're working with that comes from an external source, like a file, method parameter, or a returned value from a method you don't control.
  • Check for null-ness before dereferencing an object pointer, especially if the object came to you from an external source. This is a sub-category of the point above, but is quite common so it bears mentioning.
    • Because NullPointerExceptions are such a common failure mode, Java also contains the Optional<T> type.2 If you're writing a method that might return null in certain conditions (e.g., a failed search in a data structure), you should consider returning an Optional instead. That way, a "client" (or "user") of your method will be forced to check for the presence of the value before using it.
  • Read the documentation for methods you're using from external libraries. They will (or they should, at any rate) document the exceptions they will throw and under what conditions.
    • The corollary to this is: if you're throwing Exceptions in your own code, or returning exceptional values like null, make sure to document them clearly.

Is our method safe yet?

All right, with all that background: is our getTotalMilesRun method safe from unchecked exceptions?

Let's go through the method line-by-line. It may seem a bit tedious, but it's a good habit to get into. An escaped Exception can crash your program, the consequences of which can be simply inconvenient (e.g., a user has to restart the program), catastrophic (e.g., a user loses data), or even dangerous (e.g., a user gets injured because of a software failure in a medical device). As an example of that last, there have been multiple casualties or life-threatening events as the result of faults or exceptions in blood glucose monitoring devices or insulin pumps.

What "external" data, methods, or constructors are we using? We go through the method's components line-by-line, and I've bolded the items we're gonna need to handle.

  • fileName might be null or empty. We've handled that already.
  • new Scanner(...): We've handled the checked FileNotFoundException that's throwable by this constructor. No other exceptions are documented by the Scanner constructor, so we're good here.
  • fileScanner.nextLine(): Gets the next line. The documentation declares two possible throwable exceptions:
    • NoSuchElementException: Thrown if there isn't a next line. In this case we're okay because always we're checking hasNext() before calling it.
    • IllegalStateException: Thrown if the scanner is "closed". Let's file that away as something we need to handle!
  • We're reading a line from the file, and splitting it around a comma. This should be fine: the nextLine method wouldn't give us a null value back.
  • parts[0] and Double.parseDouble(parts[1]): Accesses the first and second element from the split pieces, and parses the second element into a double. These two lines are rife with potential program crashes!
    • parts[0]: Do we know there'll be a parts[0]? If the line is empty, this would result in an ArrayIndexOutOfBoundsException, an unchecked exception.
    • Double.parseDouble(parts[1]): This one's a double-whammy.
      • Do we know there'll be a parts[1]?
      • If there is a parts[1], what if it isn't a numeric value like 32.2, and is instead, like, a banana? This would result in a NumberFormatException, another unchecked exception.

Essentially, our program is happy with a well-formed data file like this:

Michael,5.2
Phyllis,3.8
Dwight,3.1
Pam,2.5
Jim,4.0
Oscar,6.3
Stanley,1.2

But we have no error handling for malformed data files like this:

Fun Run for the Cure

Michael,5.2
Phyllis,3.8
Dwight,3.1

Angela,missing

Pam,2.5
Jim,4.0

Daryl,three

Oscar,6.3
Stanley,1.2

Ok, so our TO-DO list is:

  1. Handle the possibility of an ArrayIndexOutOfBoundsException from trying to access parts[0] or parts[1] when the line is empty or doesn't contain a comma.
  2. Handle the possibility of a NumberFormatException from trying to parse a non-numeric value with Double.parseDouble.
  3. Handle the possibility of an IllegalStateException from the Scanner constructor.

Ask for permission, not forgiveness

How should we handle those potential failure points? Should we just go full-speed-ahead and wrap the whole method body in a try-catch block that catches all of those exceptions, handling them only if they occur?

Or should we check if our data is valid before using it?

In Java, the conventional wisdom is to "ask for permission, not forgiveness": that is, check for the conditions that would cause an exception before you use the data, rather than just trying to use it and catching the exception if it occurs.

Exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow.

Joshua Bloch. Effective Java, Chapter 10: Exceptions

However, parsing strings into numeric values is almost the only exception (hah) to this rule: the Java standard library does not provide methods to check if a String is parse-able into a number. We could roll our own, but it could be error prone, and anyway, the Double.parseDouble method already does this check. So we would end up doing the check twice.

Ok, all that said, here's our code where we're handling items #1 and #2 from our TO-DO list.

double getTotalMilesRun(String fileName) {
  if (fileName == null || fileName.isEmpty()) {
    throw new IllegalArgumentException("fileName cannot be null or empty");
  }

  double totalMiles = 0.0;

  try {
    // Create a Scanner to read the file
    Scanner fileScanner = new Scanner(new File(fileName));

    while (fileScanner.hasNext()) {
      String line = fileScanner.nextLine();
      String[] parts = line.split(",");

      // Ask permission: Check if the line is valid before proceeding
      if (parts.length < 2) {
        System.out.println("Invalid line: " + line);
        continue; // skip this line and move on to the next one
      }

      String name = parts[0];
      double miles;

      // Ask forgiveness: try to parse the double, and recover from the
      //  Exception if needed.
      try {
        miles = Double.parseDouble(parts[1]);
      } catch (NumberFormatException nfe) {
        System.out.println("Invalid miles value: " + parts[1]);
        continue; // skip this line and move on to the next one
      }

      totalMiles += miles;
    }

    // Always a good idea to close the Scanner when we're done with it.
    fileScanner.close();
  } catch (FileNotFoundException fnfe) {
    System.out.println("Could not find a file called " + fileName);
    System.out.println("Error message: " + fnfe.getMessage());
  }

  return totalMiles;
}

It's really important to do something when handling an exception—there are few things worse in software development than silent software failures. For now, we're simply printing an error message and skipping invalid lines. In a real project, you might do something else, depending on requirements.

For example, the read_csv function in the pandas library will raise an error by default if it encounters malformed lines while reading a file, but also lets the user decide if they want some different response to malformed lines.

On to item #3 from our TO-DO list! We see that the Scanner constructor might throw an IllegalStateException if the Scanner is "closed". What does that mean?

Well, when we use a Scanner to read a file, we're obtaining a resource (i.e., a file handle) from the operating system. When we're done with that resource, we need to "close" it, which tells the operating system that we're done with it and that it can be freed up for other uses.

In the code above, we're doing this with the fileScanner.close() method call. After we've closed the Scanner, it can no longer be used to read lines from the file.

In our case, we are clearly closing the Scanner after using it to read lines. So, in terms of the IllegalStateException from the Scanner constructor, we're okay.

With that, we've addressed our TO-DO list of items.

Does that mean that nothing can possibly go wrong with our getTotalMilesRun method? Of course not! The file might be locked by another process, or it might be on a network drive that has gone offline, or it might have so many records that our totalMiles variable overflows, or ... ... ...

The point is, you can never be sure that your code is 100% safe from exceptions, but you can do your best to handle the ones you know about, and to check for conditions that might cause exceptions before they occur.

In this case, we've done a fairly good job being defensive about the data we're working with, and we've handled the exceptions that we know about.

However, there's one last thing to consider. If something unforeseen does go wrong while reading the file, an exception would occur and jump to the catch block, or escape the method. Either way, we're going to be stuck with an open Scanner, because we wouldn't reach the fileScanner.close() method call in these scenarios. This will leave us with an open file handle, which can lead to resource leaks and other problems.

Can we make sure that our Scanner always closes, no matter what?

The finally block, discussed in the next lesson, can be used for this purpose.

Optional additional reading about error handling

Different languages, communities, and teams have differing views about exceptions.


  1. Note that this imagined "other" programmer might well be you, working on a different part of the same code base.

  2. If you are not sure what the <T> means here, please see the notes about generics or type parameters in the lesson on lambdas.