Overview of Java Exceptions and How to Handle Them

Java Exceptions are disruptions that occur during the execution of a program, potentially leading it to terminate abruptly. Exception handling enables programmers to foresee and address these issues, directing the program along alternative paths when an exception occurs.

What are Java Exceptions

A Java program can run into problems that result in the program being abruptly terminated during its execution. These problems are called exceptions.

A good programmer should be able to recognize the errors that may occur during execution and provide alternate routes for the program to take in case of such exception. This practice is called exception handling.

Now you may be wondering why do we need exception handling at all. Why not write programs that won’t throw exceptions?

Why do We Need Exception Handling

As it turns out, writing programs that won’t throw exceptions is not as easy as it sounds. Most of the time, these unavoidable errors are out of the programmer’s control.

Programs that accept user input are prone to running into exceptions because of an invalid input the user provides. So is reading external files considering the chance that they have been moved, renamed, or deleted by an outside source without the knowledge of the programmer.

In such cases, the program must be able to handle the exception gracefully without terminating the execution.

Hierarchy of Java Exceptions

All the exceptions in Java should be a child of the Exception class, which itself is a child of the Throwable class.

Two main subclasses of the Exception class are RuntimeException and IOException.

Exception vs Error

Another child class of the Throwable class is the Error class. However, errors are different from exceptions.

Errors indicate problems that the JVM may run into during execution. These problems are usually critical and irrecoverable. Memory leaks and library incompatibility issues are common reasons for errors in programs.

StackOverflowError and OutOfMemoryError are two examples of Java Errors.

Checked and Unchecked Exceptions

We can divide Java Exceptions into two main categories: checked and unchecked exceptions.

Checked exceptions are the exceptions that need to be handled in the program before compiling. If these exceptions are not handled, the program won’t be compiled by the Java compiler. Therefore, these are also called compile-time exceptions. IOExceptions are good examples of checked exceptions.

Unchecked exceptions are the exceptions that the compiler ignores when compiling the program. Whether we have handled these exceptions in the program or not doesn’t matter when the program is compiled. Since exception handling is not imposed on these exceptions, our program can run into RuntimeExceptions that result in program termination.

All the classes that extend the RuntimeException class are unchecked exceptions. Two examples of such classes are NullPointerException and ArrayIndexOutOfBoundsException.

Commonly Used Methods in the Exception Class

We’ll go through a few commonly used methods in the Java Exception class:

  1. getMessage: returns a message containing details about the exception that occurred.
  2. printStackTrace: returns the stack trace of the exception occurred.
  3. toString: returns the class name and the message that is returned with getMessage method.

How to Handle Exceptions

Let’s see how we can handle exceptions in Java:

try-catch

We can catch exceptions and handle them properly using a try-catch block in Java.

In this syntax, the part of the code that is prone to throwing an exception is placed inside a try block and the catch block/blocks catch the thrown exception/exceptions and handle them according to a logic we provide.

The basic syntax of a try-catch block is as follows:

try {
    //exception-prone code
}
catch(Exception e) {
    //error handling logic
}

With this approach, the program does not halt execution when an exception is thrown by the program, instead, it is gracefully handled.

We’ll see how to handle the IOExceptions thrown by the FileReader class in a Java program.

Example:

import java.io.FileReader;

public class TryCatchBlockExample {

    public static void main(String[] args) {

        try {
            FileReader file = new FileReader("source.txt");
            file.read();
        }
        catch(Exception e) {
            e.printStackTrace();
        }
    }
}

Here, we have used a single catch block to handle the FileNotFoundException thrown when instantiating the FileReader class and IOException thrown by the read() method of the FileReader class.

Both of these exceptions are children of the Exception class.

We can also use multiple catch statements to catch different types of errors thrown by the code inside the single try statement. For the previous example, we can use one catch block to catch the FileNotFoundException and another catch block for the IOException as the following code snippet shows:

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class TryCatchBlockExample {

    public static void main(String[] args) {

        try {
            FileReader file = new FileReader("source.txt");
            file.read();
            file.close();
        }
        catch(FileNotFoundException e) {
            e.printStackTrace();
        }
        catch(IOException e) {
            e.printStackTrace();
        }
    }
}

If the thrown exception matches the exception handled by the first catch statement, it is then handled by the logic inside the first catch statement.

If the exceptions do not match, it is passed on to the second catch statement. If there are more than two catch statements, this process continues until the exception reaches a catch statement that catches its type.

Since FileNotFoundException is a subtype of IOException, using the 2nd catch statement to catch a FileNotFoundException won’t work. It will be handled by the first catch statement and never reach the 2nd statement.

Note: It is compulsory to use at least one catch statement with a try statement.

finally

When we use a try-catch block to catch exceptions in our program, there are instances we want to implement some logic despite the fact whether an exception was caught or not. In such cases, we can use a try-catch-finally block instead of just a try-catch block.

Then, the code inside the finally statement is implemented whether or not an exception occurs. The finally statement should always come at the end of the try-catch-finally block.

For example, when we use the FileReader class to read a file, it is essential to close the opened file at the end of processing whether an exception occurs or not. To ensure this, we can place the code to close the file inside a finally statement.

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class TryCatchFinallyBlockExample {

    public static void main(String[] args) {
        FileReader file = null;
        try {
            file = new FileReader("source.txt");
            file.read();
        }
        catch(FileNotFoundException e) {
            e.printStackTrace();
        }
        catch(IOException e) {
            e.printStackTrace();
        }
        finally {
            file.close();
        }
    }
}

However, if you try to compile the above code, the code won’t be compiled due to an unhandled IOException. This is because the close() method of the FileReader class can also throw IOExceptions. So, we have to place this part inside another try block like this:

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class TryCatchFinallyBlockExample {

    public static void main(String[] args) {
        FileReader file = null;

        try {
            file = new FileReader("source.txt");
            file.read();
        }
        catch(FileNotFoundException e) {
            e.printStackTrace();
        }
        catch(IOException e) {
            e.printStackTrace();
        }
        finally {
            try {
                file.close();
            }
            catch(IOException e) {
                e.printStackTrace();
            }
        }
    }
}

throws

Handling errors using the throws keyword in Java is simple. In fact, in this approach, you don’t really handle the exception at the place it occurs. Instead, we throw the exception out of the current method to the method that called the current method. Then, handing the error becomes a responsibility of the outer method.

To throw an exception out of a method, you simply have to declare that this method may throw the considered exception. Let’s see how we can handle IOExceptions thrown by the FileReader class using this approach.

Example:

import java.io.FileReader;
import java.io.IOException;

public class ThrowsExample {

    public void readFile throws IOException {
        FileReader file = new FileReader("source.txt");
        file.read();
        file.close();
    }
}

throw

Unlike other approaches in this list, the throw keyword is not used to handle errors. But since most people confuse the throw keyword with throws keyword, we thought it would be best to discuss it here.

The throw keyword is used to explicitly invoke an exception. We can throw a newly instantiated exception or an exception that was caught inside the method.

public class ThrowExample {

    public void invalidate(int amount) throws Exception {
        if (amount < 500) {
            throw new Exception("Amount not sufficient");
        }
    }
}

User-Defined Exceptions

In addition to using built-in Java exceptions, you can define your own exceptions. You can define them as either checked or unchecked exceptions. To create a new checked exception, your new exception should extend the Exception class.

To create an unckecked exception, extend the RuntimeException class.

In the following code example, we have created a user-defined checked exception:

public class InvalidLengthException extends Exception {
    private int length;
    private String message;

    public InvalidLengthException(int length, String message) {
        this.length=length;
        this.message=message;
    }

    public int getAmount() {
        return this.length;
    }

    public String getMessage() {
        return this.message;
    }
}

Now we can use the above exception inside our program logic like this:

public class InputChecker {

    private int minLength;
    private int maxLength;

    public InputChecker(int minLength, int maxLength) {
        this.minLength=minLength;
        this.maxLength=maxLength;
    }

    public void checkStringLength(String strInput) throws InvalidLengthException {
        int strLength = strInput.length();
        if (strLength < minLength) {
            throw new InvalidLengthException(strLength, "Input should have minimum "+minLength+" characters");
        }
        else if (strLength > maxLength){
            throw new InvalidLengthException(strLength, "Input should have maximum "+maxLength+" character");
        }
    }
}

If we check the length of a string using the InputChecker class, it will throw a InvalidLengthException if the string length is below the minimum length or above the maximum length.

public class Main {

    public static void main(String[] args) {
        InputChecker ic = new InputChecker(2, 7);
        try {
            ic.checkStringLength("longer than the maximum length");
        }
        catch(InvalidLengthException e) {
            e.printStackTrace();
        }
    }
}

When we run the above code snippet, it will throw an InvalidLengthException and we will get the following output:

InvalidLengthException: Input should have maximum 7 character
    at InputChecker.checkStringLength(InputChecker.java:17)
    at Main.main(Main.java:6)

Conclusion

Handling errors well in Java is essential for smooth program running and a better user experience. Using try-catch-finally blocks and custom exceptions helps you manage your app more effectively.