Exception Handling in Java

Without exceptions, every method would need to return special error codes, and every caller would need to check them, error handling would be tangled up with normal logic. Exceptions help on this!

Learning objectives

By the end of this reading, you will be able to:

  • Explain what a Java exception is and how it differs from a compilation error.
  • Read a stack trace and identify where an exception was thrown.
  • Use try, catch, and finally to handle exceptions (including multiple types).
  • Distinguish checked from unchecked exceptions and explain how the compiler treats each.
  • Define and throw your own custom exception class.
  • Decide whether a new exception should be checked or unchecked.

What are Java Exceptions ?

Java Exceptions are events that occur during the execution of Java programs that disrupt the normal flow of the execution (e.g., divide by zero, array access out of bound, null pointer exceptions, etc.).

It is considered a good practice to use exceptions in Java so that we can separate error-handling code from regular code.

You can think about Exceptions as runtime errors that might or might not manifest at runtime, depending on the inputs that the program receives.

Exceptions are different from compilation errors that arise at compilation time (before running the program) due to an incorrect Java syntax.

A very common runtime exception is the infamous NullPointerException.

For example, consider this method:

static int sumLengths(String s1, String s2) {
    return s1.length() + s2.length();
}

What happens if the method is invoked passing a null reference as a parameter? For example:

String s2 = null;
System.out.println(sumLengths("hello", s2));

An exception is thrown and the program crashes:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "s2" is null
	at ExampleNPE.sumLengths(ExampleNPE.java:10)
	at ExampleNPE.main(ExampleNPE.java:6)

This is because Java cannot invoke s2.length() if s2 is null.

This output is called a stack trace. Read it top-to-bottom: the first line names the exception type and a human-readable message; the indented at ... lines show the chain of method calls active when the exception was thrown, with the deepest call (where the exception actually originated) at the top. Here, the exception fired inside sumLengths at line 10, which was called from main at line 6.

An Interesting fact about NullPointerException!

Speaking during a software engineering conference in 2019, Tony Hoare the creator of the null reference publicly apologized for inventing it, as NullPointerException caused too many software crashes.

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.” Tony Hoare.



In Java, an exception is an object! (surprised? I doubt it!… you should know by now that in Java almost everything is an object 😃 except primitive types like int, double, float ) that wraps an error event that occurred at runtime.

For example, let’s consider this simple program:

public class Main {
    public static void main(String[] args) {
        int[] array = new int[3];
        array[0] = 10;
        array[1] = 20;
        array[2] = 30;
        System.out.println(array[3]);
        System.out.println("program ended");
    }
}

If you run it, you will get a runtime exception:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
    at Main.main(Main.java:7)

This is because the statement System.out.println(array[3]); is trying to access the 4th element of an array that has only three elements (remember that array indexes start from 0, so array[3] is accessing the fourth element).

This interrupts the program execution at the line of code where the exception was thrown (line 7 of Main.java). In fact, the program does not print System.out.println("program ended");.

A Java exception usually contains:

  • Information about the error including its type (e.g., ArrayIndexOutOfBoundsException ), and the class, method and line number that triggered the exception (e.g., at Main.main(Main.java:7)),

  • a stack trace showing where it was thrown. Some exception messages include extra details, such as an invalid index.






How to handle exceptions?

In general, good programmers handle exceptions when they can recover, report useful information, or clean up resources.

Exceptions are handled by catching them!

This is accomplished with the try and catch statements. You cannot have a catch without a try; a try block must be followed by catch, finally, or both.

If we want to catch the exceptions in the previous program, we can do it like this:

int[] array = new int[3];
array[0] = 10;
array[1] = 20;
array[2] = 30;

try {
    System.out.println(array[3]);
    System.out.println("this will not be printed because an exception is thrown");
} catch(ArrayIndexOutOfBoundsException e){
    System.out.println("You tried to access an element of the array that does not exist");
}

System.out.println("program finished");

When the ArrayIndexOutOfBoundsException exception occurs within the try section, the program will execute the code inside the catch section. In this case, we are merely printing an error message. Generally, depending on what we’re doing, we might try to do some actions to recover from the exception or log some important information.

public class Main {
    public static void main(String[] args) {
        int[] array = new int[3];
        array[0] = 10;
        array[1] = 20;
        array[2] = 30;

        try {
            System.out.println(array[3]);
            System.out.println("this will not be printed because an exception is thrown");
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("You tried to access an element of the array that does not exist");
        }

        System.out.println("program finished");
    }
}

Now the program will print:

You tried to access an element of the array that does not exist
program finished

There are a few important points to notice:

  1. Now the program reaches the end! This is because by catching the exception, the program is able to handle the exception and therefore does not crash.

  2. Because ArrayIndexOutOfBoundsException belongs to the package java.lang we don’t need to import the class (e.g., import java.lang.ArrayIndexOutOfBoundsException). All the classes in the package java.lang are imported by default (e.g., Object, String, Integer).

  3. The catch will only catch the exception of type ArrayIndexOutOfBoundsException. If other exception types are thrown, these will not be caught by the catch statement.

  4. ArrayIndexOutOfBoundsException e is declaring object reference variable e of type ArrayIndexOutOfBoundsException, this allows us to refer to the exception that has occurred! We can get whatever information we want out of the exception, for example the error message (e.g., e.getMessage()).

Optionally, you can also have a finally section after the catch, where you can put the code that you want that is always executed no matter if the exception is thrown or not:

try {
    // Try to do something in this section...
} catch(Exception e) {
    // Some exception was thrown in the section above, handle the exception here.
} finally {
    // Regardless of whether an exception was thrown, execute this section.
}

An example why we would want to use the finally section, is if we are trying to read a file. Regardless of whether an exception occurs or not, it’s important to close() the file.

A modern alternative: try-with-resources. Since Java 7, anything that implements AutoCloseable (files, streams, sockets, database connections) can be declared in the try header, and Java will close it automatically, no finally needed:

try (FileReader reader = new FileReader("data.txt")) {
    // use reader...
} catch (IOException e) {
    System.out.println("Could not read file: " + e.getMessage());
}

This is the idiomatic way to handle files in modern Java; reach for explicit finally { close(); } only when you cannot use try-with-resources.

Good programmers do not catch every exception. They catch exceptions when they can recover, report a helpful error, clean up resources, or add useful context. If the program cannot handle the problem, it is often better to let the exception propagate.



Edit Get your hands dirty!

This program computes the divisions among the elements of two arrays of integers. Unfortunately, one array contains 0 and the program will throw an exception at line 10:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at exception.Main.main(Main.java:12)

Catch the exception ArithmeticException and print “error division by zero” so that the program can continue to print the remaining divisions.

package exception;

public class Main {
    public static void main(String[] args) {
        int[] numerators = {10, 20, 30, 40, 50};
        int[] denominators = {2, 4, 0, 5, 10};

        for (int i = 0; i < numerators.length; i++) {
            int result = numerators[i] / denominators[i];
            System.out.println(
                numerators[i] + " / " + denominators[i] + " = " + result
            );
        }
    }
}






How to deal with multiple exceptions?

Let’s say that in the same method we want to catch both NullPointerException and ArrayIndexOutOfBoundsException.

We can simply do like this:

try {
    // Code block
} catch (NullPointerException e1) {
    // Handle NullPointerException exceptions
} catch (ArrayIndexOutOfBoundsException e2) {
    // Handle ArrayIndexOutOfBoundsException exceptions
}

In this case different code can be executed to handle the different exception types.

If you want to catch different exception with only one catch you can do like this:

try {
    // Code block
} catch (NullPointerException | ArrayIndexOutOfBoundsException e1) {
    // Handle NullPointerException and  ArrayIndexOutOfBoundsException exceptions
}




Exceptions can be caught together by specifying the generic type.

try {
    // code
} catch (RuntimeException e) {
    // Handle all runtime errors
}

In this case, it will catch both NullPointerException ArrayIndexOutOfBoundsException. But it will also catch all Runtime Exceptions!

Edit Warning

Be careful about being too generic in the types of exceptions you catch. You might end up catching an exception you did not know would be thrown. As such, you might hide errors that you are not aware of.



Edit Common anti-patterns to avoid

  • Empty catch blocks. Silently swallowing an exception hides the failure and makes the bug invisible. At a minimum, log the exception (e.g., e.printStackTrace() or a proper logger) so you can see something went wrong.

  • Catching Exception (or worse, Throwable) just to make code compile. This hides bugs you did not anticipate and may even catch Errors like OutOfMemoryError. Catch the most specific exception type you can actually handle.

  • Catching and rethrowing without context. If you must rethrow, wrap the original cause so the stack trace is preserved: throw new MyException("could not load config", e);, never just throw new MyException("error") after catching e.






Checked vs Unchecked exceptions

This is the hierarchy of Throwable objects:

All exceptions and errors extend from a common java.lang.Throwable parent class. Only instances of Throwable (and sub-classes) can be thrown and caught.

The exceptions that we have seen so far (NullPointerException, ArrayIndexOutOfBoundsException, and ArithmeticException) extend java.lang.RuntimeException.

Besides RuntimeException, there are other classes that extend java.lang.Throwable. The hierarchy splits into three groups:

  • java.lang.Error, serious problems that a normal program is not expected to recover from, like OutOfMemoryError. You generally do not catch these.

  • java.lang.RuntimeException (and its subclasses), unchecked exceptions. Typically signal bugs in the program (a null reference, a bad array index, dividing by zero).

  • All other java.lang.Exception subclasses, checked exceptions. Typically signal conditions outside the program’s control that callers should anticipate (a missing file, a failed network read).

The key distinction is checked vs unchecked, and it determines how the compiler treats the exception.

Unchecked exceptions mean that the Java compiler does not enforce that developers handle them explicitly. In fact, it is up to you to catch them and do something about them – but only if you want.

Checked exceptions mean that the Java compiler enforces that developers must handle them with try and catch. Alternatively, methods that generate checked exceptions must declare that they (re-)throws them so that the callee method becomes responsible to handle them.

Examples of checked exceptions are java.io.IOException and java.io.FileNotFoundException, which are thrown when files are related to IO (input/output) operations.

  Checked Unchecked
Extends Exception (but not RuntimeException) RuntimeException
Compiler enforcement Must be caught or declared with throws No compiler enforcement
Typical cause Conditions outside the program’s control (I/O, network) Bugs in the program (null reference, bad index, bad argument)
Caller can recover? Often yes, caller is expected to anticipate it Usually no, caller should have prevented it
Examples IOException, FileNotFoundException, SQLException NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException, IllegalArgumentException

For example, if we try to compile this program, we will have the following three compilation errors:

import java.io.FileReader;
import java.io.BufferedReader;

public class Main {
    public static void main(String[] args) {

        FileReader fileReader = new FileReader("to_be_read.txt");
        BufferedReader bufferedReader = new BufferedReader(fileReader);
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            System.out.println(line);
        }
        fileReader.close();
    }
}
Main.java:7: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
    FileReader fileReader = new FileReader("to_be_read.txt");
                            ^
Main.java:10: error: unreported exception IOException; must be caught or declared to be thrown
            while ((line = bufferedReader.readLine()) != null) {
                                                  ^
Main.java:13: error: unreported exception IOException; must be caught or declared to be thrown
            fileReader.close();
                            ^
3 errors
compiler exit status 1

The problem is that the mentioned method calls can throw checked exceptions.

An easy fix is to declare that the method main can throw the exceptions like this:

public static void main(String[] args) throws FileNotFoundException, IOException {
    ...
}

But this isn’t ideal, as we should write some code that deals with the exceptions.



Edit Get your hands dirty!

Modify the program below to resolve the compilation errors, by handling the exceptions with try and catch.

In the catch section, you should print error messages.

Note that this time, the exception types do not belong to the java.lang package. Therefore, you need to import them like this:

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileReader;
import java.io.BufferedReader;

public class Main {
    public static void main(String[] args) {

        FileReader fileReader = new FileReader("to_be_read.txt");
        BufferedReader bufferedReader = new BufferedReader(fileReader);
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            System.out.println(line);
        }
        fileReader.close();
    }
}



You should know that there is a lot of controversies around checked versus unchecked exceptions. For instance, C# does not have checked exceptions. Many developers think that checked Exceptions make the code verbose. Others think that checked exceptions are important to enforce programmers handle potential problems.






Create your own exceptions

It is often important to be able to create your own exceptions that are specific to your application. This is because they will be much more informative to developers. Also, you could add helper methods to your exception class, to provide more meaningful error information.

There are four steps to define your own exception class:

  1. Choose a meaningful name that self-describes the Exception. For convention, it must end with Exception.

  2. Create a Java file with a public class declaration with that name.

  3. Decide if the exception should be checked or unchecked:

    • If checked, the class must extend Exception,
    • Otherwise (if unchecked), the class must extend RuntimeException.
  4. Define constructor(s) that call into super’s constructor(s).

For example, let’s declare a checked exception that indicates that we are trying to sell a Book that is sold out.

public class InsufficientBookCopiesException extends Exception {
    private final String bookTitle;

    public InsufficientBookCopiesException(String bookTitle) {
        super("Trying to sell a sold-out book: " + bookTitle);
        this.bookTitle = bookTitle;
    }

    public String getBookTitle() {
        return bookTitle;
    }
}

Now that we have created an exception, how to throw it at runtime? Like this:

throw new InsufficientBookCopiesException("Harry Potter and the Philosopher's Stone");

For example:

if(numCopies == 0) {
    throw new InsufficientBookCopiesException(bookTitle);
}

How to decide if you should define a checked or unchecked exception?

The official Java Documentation provides guidance on when to use checked and exceptions:

“If a client can reasonably be expected to recover from an exception, make it a checked exception. If a client cannot do anything to recover from the exception, make it an unchecked exception.”

https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html

For example, before we open a file, we can first check the input file name. If the user input file name is invalid, we can throw a custom checked exception:

if (!isCorrectFileName(fileName)) {
    throw new IncorrectFileNameException("Incorrect filename : " + fileName );
}

This is a recoverable exception (the user can provide an alternative or default file name)

However, if the input file name is a null pointer or it is an empty string, it means that we have some errors in the code. In this case, we should throw an unchecked exception, and for invalid arguments like this, Java already provides a standard one, IllegalArgumentException:

if (fileName == null || fileName.isEmpty())  {
    throw new IllegalArgumentException("The filename is null or empty.");
}

Tip: prefer using built-in exceptions (IllegalArgumentException, IllegalStateException, NullPointerException, etc.) when they fit. Only define your own exception class when no standard exception captures the meaning.






Check your understanding

Before class, see if you can answer these. We will discuss them together, there are no answers below on purpose.

  1. What is the difference between a compilation error and a runtime exception? Can a program with no compilation errors still throw an exception?

  2. Given the stack trace below, on which line did the exception originate, and which method called the method that threw it?
     Exception in thread "main" java.lang.NullPointerException
         at com.example.User.getName(User.java:42)
         at com.example.Greeter.greet(Greeter.java:17)
         at com.example.Main.main(Main.java:8)
    
  3. Why is it generally a bad idea to write catch (Exception e) { } with an empty body?

  4. Suppose a method parseConfig(String path) reads a configuration file from disk. It can fail because (a) the file does not exist, or (b) the caller passed null as the path. Should each of these be a checked or unchecked exception? Why?

  5. What does the finally block guarantee that a catch block alone does not?

  6. Look at the custom InsufficientBookCopiesException class. Why does it extend Exception rather than RuntimeException? What would change if we made the opposite choice?






Key takeaways

  • An exception is an object that represents an error event at runtime; throwing one interrupts normal control flow.
  • The stack trace tells you the exception type, a message, and the chain of method calls, read it top-to-bottom, deepest call first.
  • Handle exceptions with try / catch / finally. Use try-with-resources for anything that needs to be closed.
  • Unchecked exceptions (RuntimeException and subclasses) usually signal bugs and are not enforced by the compiler. Checked exceptions usually signal external failures and must be caught or declared with throws.
  • Define your own exception class when you want to convey domain-specific meaning; otherwise reuse Java’s built-in exceptions.
  • Rule of thumb (from the Java tutorial): if the caller can reasonably recover, make it checked; otherwise, make it unchecked.