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:
getMessage
: returns a message containing details about the exception that occurred.printStackTrace
: returns the stack trace of the exception occurred.toString
: returns the class name and the message that is returned withgetMessage
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.