Exception handling in Java is a mechanism that allows developers to manage and respond to exceptional conditions or errors that may occur during the execution of a program. Exception handling helps ensure the robustness and reliability of the software by providing a structured way to deal with unexpected situations. Like most modern programming languages, Java offers advanced exception handling features that allow programmers to handle both errors and exceptions. However, detecting exceptions in production and fixing the ones that impact performance and user experience is a hard problem.
Overview of Exceptions in Java
What is a Java Exception?
An exception is an abnormal condition that occurs during the execution of a program and disrupts the normal flow of the program’s instructions. Exceptions in Java are objects that represent exceptional conditions. Throwable is the superclass of all exceptions and errors in Java. Exceptions occur for many reasons, some of which are invalid user input, coding errors, opening an unavailable file, loss of network connection, etc.
Types of Exceptions in Java
Generally, there are two kinds of Java exceptions:
- Checked exceptions (compile time exceptions)
- Unchecked exceptions (runtime exceptions)
Common Terminology of Java Exception Handling
try and catch: The “try” block is used to enclose the code where exceptions might occur. It’s followed by one or more catch blocks. The “catch” block is used to handle a specific type of exception that may be thrown within the associated “try” block.
finally: The “finally” block is used to define code that needs to be executed regardless of whether an exception was thrown or not. It’s optional.
throw and throws: The “throw” keyword is used to explicitly throw an exception from a method or block of code. The “throws” keyword is used in the method declaration to specify that the method may throw one or more types of exceptions. The caller of the method must handle these exceptions or declare them in its own throws clause.
Stack Trace: The stack trace is a trace of method calls that led to the exception. It helps identify the sequence of method calls that resulted in the exception.
Call Stack: The call stack is a data structure that JVM uses to keep track of the method calls. The call stack is made up of stack frames, one for each method call. When the execution enters a method, a new frame is created and pushed onto the call stack. Similarly, when the execution exits a method, its frame is popped from the call stack. A stack frame usually stores local variables and arguments passed into the method and some metadata.
Exception Class and Hierarchy
In Java, the Exception class is a fundamental part of the exception handling mechanism. It’s a subclass of the Throwable class and serves as the base class for all exceptions. The Exception class identifies the type of event that occurred. Exception classes are part of the inheritance hierarchy, just like every other Java class. Exceptions must extend java.lang.Exception or one of its subclasses. By extending the Exception class or any of its subclasses, you can create your own exception classes.
Exception Handling
Throwing an Exception
When an exception occurs within a method, the method creates an exception object and hands it off to the runtime system. This is called “throwing an exception” and is done using the keyword “throw” to transfer control from the current execution point to an appropriate exception handler. The exception object contains information about the event, including its type and the state of the program when the event occurred. An exception object is a member of an Exception class and can be caught and handled by the program.
Exception Handling
After a method throws an exception, the runtime system attempts to find something to handle it. The set of possible “somethings” to handle the exception is the ordered list of methods that had been called to get to the method where the exception occurred. The list of methods is known as the call stack (see the next figure).
The runtime system searches the call stack for a method that contains a block of code that can handle the exception. This block of code is called an exception handler. The search begins with the method in which the exception occurred and proceeds through the call stack in the reverse order in which the methods were called. When an appropriate handler is found, the runtime system passes the exception to the handler, as shown in the below figure. An exception handler is considered appropriate if the type of the exception object thrown matches the type that can be handled by the handler.
The exception handler chosen is said to catch the exception. If the runtime system exhaustively searches all the methods on the call stack without finding an appropriate exception handler, the runtime system (and, consequently, the program) terminates.
Try-With-Resources
Introduced in Java 7, the try-with-resources statement is used to automatically close resources at the end of a block of code. Resources that implement java.lang.AutoCloseable or java.io.Closeable can be used with this construct. This helps avoid memory leaks and ensures proper cleanup of resources.
try (ResourceType resource = new ResourceType()) {
// Use the resource
} catch (Exception e) {
// Handle the exception
}
Multi-Catch
Java 7 introduced multi-catch, allowing a single catch block to handle multiple types of exceptions. This simplifies exception handling when the same handling logic applies to different exception types.
try {
// Some code that may throw exceptions
} catch (IOException | SQLException e) {
// Handling code for both IOException and SQLException
}
Improved Type Inference
With Java 10 and later versions, local variable type inference (var) was enhanced. This allows the use of var in the try-with-resources statement, improving code readability and conciseness.
try (var resource = new ResourceType()) {
// Use the resource
} catch (Exception e) {
// Handle the exception
}
Optional Class
Although not directly related to exception handling, the Optional class (introduced in Java 8) is often used in exception related scenarios. It provides a way to handle cases where a value may be absent without using null.
Optional<String> maybeValue = Optional.ofNullable(getValue());
if (maybeValue.isPresent()) {
String value = maybeValue.get();
// Handle the value
} else {
// Handle the absence of a value
}
Also Read: Null Pointer Exception In Java – Explained | How To Avoid and Fix
Checked and Unchecked Exceptions in Java
Java supports checked and unchecked exceptions.
- Checked Exception
The classes that directly inherit the Throwable class except RuntimeException and Error are known as checked exceptions. For example, IOException, SQLException, FileNotFoundException, etc. Checked exceptions are called compile time exceptions because these exceptions are checked at compile time by the compiler. If a code within a method throws a checked exception, then it should either be handled by the method or specify it using the throws keyword.
- Unchecked Exception
The classes that inherit the RuntimeException are known as unchecked exceptions. For example, ArithmeticException, NullPointerException, ArrayIndexOutOfBoundsException, NumberFormatException, etc. Unchecked exceptions (also known as runtime exceptions) are not checked at compile time, but they are checked at runtime. Though a runtime exception is not required to be handled it does not mean that you do not need to be concerned about it.
How to Handle an Exception?
Handling Java exceptions happens with the try, catch, and finally blocks. They can be used to define how to handle exceptions when they occur.
Try blocks should include code that might throw an exception. If your code throws more than one exception, you can choose whether to:
- Use a separate try block for every statement that may throw an exception or
- Use one try block for several statements that could throw multiple exceptions
There is no limit to adding catch blocks, as you can add as many catch blocks as you want. However, you can add only one finally block to each try block.
Each catch block should handle exceptions thrown by the try blocks. The finally block always executes whether or not an exception is thrown after the successful execution of the try block or after one of the catch blocks.
After the try catch block, programmers usually use finally block to do cleanup.
How to Specify an Exception?
If you don’t handle an exception within a method, it will propagate in the call stack. For checked exceptions, you need to either handle it or specify that the method might throw it. If you want to throw it, add a throws clause to the method declaration to accomplish this. As a result, all calling methods must handle or specify the exception themselves. Similarly, if you wish to specify that a method might throw an unchecked exception, you may do so.
public void doSomething(String input) throws MyBusinessException {
// do something useful ...
// if it fails
throw new MyBusinessException("A message that describes the error.");
}
Knowing whether to Handle or Specify an Exception
It usually depends on the use case whether you should handle or specify a Java exception. As you might expect, it is difficult to provide a recommendation that works for all use cases.
For this, you can generally ask yourself the following questions:
- Can you handle the exception within your current method?
- Can you anticipate the needs of all your class members? And would handling the exception fulfill these needs?
If the answer to both questions is “yes”, you should handle the exception within your current method. In all other situations, it’s best to specify it. This allows the caller of your class to implement the handling according to the current use case.
Finding and Fixing Exceptions in Java with Seagence
The purpose of exceptions and exception handling is to try and recover the application from abnormal events. In case recovery is not possible, the application should notify the user and log the exception to the log file. When a defect occurs, the logged exceptions help developers quickly debug and detect the root cause to avoid business impact. Production applications throw numerous exceptions during runtime. Because all exceptions are not logged in the log file, debugging becomes a painful job. This job becomes even more painful when the exception causing the defect is swallowed or not logged in the log file. Sometimes defect causing exceptions leave no trace in the log file if the proper logging level is not enabled in production.
Seagence Automatically finds Java Defects with Root Cause
Seagence automatically helps you find and fix exceptions in Java. It is a Realtime Defect Detection and Resolution tool that proactively detects known and unknown defects using a unique approach including rootcause in realtime, and eliminates the need for debugging. See the above figure with the Seagence detected list of defective transactions, list of exceptions thrown during the execution of the expanded transaction, and the root cause exception causing the defect highlighted. Seagence records all exceptions (handled, unhandled, and swallowed), errors thrown by the application, and indexes them for easy searching, so you lose no detail if debugging becomes necessary. With the Seagence detected defects and rootcauses in hand, you can quickly fix your broken code with greater speed and accuracy. Click here to know more or try Seagence free here.