Exception

During the execution of applications, we often encounter exceptions such as NullPointerException, ArrayIndexOutOfBoundException, IOException, and numerous other errors. These can sometimes halt the entire application. In this article, we aim to explore and understand the concepts of Exceptions and Errors in Java.

Let's understand what is exception.
According to Oracle's definition, "An exception is an event, which occurs during the execution of a program, that disrupts the normal flow of the program's instructions."
An Exception indicates that something went wrong during execution, like requiring resources not available, our logic computations performing invalid operations like accessing out of the bound index in an array, performing some operations on null, or dividing a number by zero. It's proper handling helps to make our application robust.
In Java, it is represented by an Object which holds details about the error. Like error type, in which file, method, and line it happened, state of the program when it happened.

How exception handling works in Java.

  • When an error happens our program creates an exception object and handover it over to JRE (Java Runtime Environment). This process is called throwing an exception.
  • JRE checks where it happened (in which class and method) and from that place, fetches all the methods one after another to reach the current method where the exception happened. This list of methods is called stack trace.
  • JRE searches over this stack trace and tries to find if any exception handler is available for it. If it found any handler it handed over it. We can say it is exception catching and handling.
  • In case JRE does not find any handler it will stop the execution of the program and print that error in logs.

Some Exceptions we as a developer can handle and can recover from the failure, but some of them we cannot handle. To know more details about it we need to understand the hierarchy of exceptions.

  • We first have the Throwable class, which is the parent class of all Exceptions. It holds information related to stack traces and error messages.
    • Here stack trace is represented by the class StackTraceElement, which holds information about the class name, method name, file name, and line number where the error happened.
    • So, Throwable holds an array of StackTraceElement[]. This array we called stacktrace.
  • 2 subclasses of Throwable are Error and Exception.
  • Error: Such kinds of exceptions are external to our application, these errors are not recoverable and the developer should not handle them during application execution using a try-catch block. It happens because of some critical issues outside of application code, we need to identify them and correct them as soon as possible before application execution.
    • Serious, hard errors in the system, such as those that prevent the JVM from running.
    • It is represented by the Error class and does not hold any logic within itself, all logic it inherits from the Throwable class.
    • There are various subclasses for Error like IOError, OutOfMemoryError, and StackOverflowError.
    • For example, if there is no memory available for JVM.
  • Exception: it is represented by the Exception class, such errors can be recoverable and the developer must handle it using a try-catch block. The developer must catch it and can provide messages to the user or write some logic to recover from it.
    • Exception class does not hold any logic all it inherits from the Throwable class.
  • RuntimeException class is a subclass of the Exception class. It represents exceptions that occur because of our application code logic issues. It happens at runtime and is internal to our application. It mainly happens because of improper programming usage. Example,
    • Executing method of an object may be null and can cause NullpointerException. Better to use a null check before calling any method of such an object.
    • Accessing out of bound index in an array, which may result in ArrayIndexOutOfBoundException, can be prevented by checking if 0>index> arr.length
    • Casting one class into another non-compatible class may cause ClassCastException. It can be prevented by using the instanceOf keyword.

All these exceptions are also categorized into 2 types checked and unchecked.
  • Checked: all exceptions are checked except Error and RuntimeException and its subclasses.
  • Unchecked: Error and RuntimeException come under it.

It is best practice to not handle Throwable, Error, subclasses of Error, Runtime Exception, and subclasses of RuntimeException using try-catch block, instead, we should handle and fix their causes.

Now let's understand within our code how can we handle exceptions.

  • 'try-catch' block
    • Java provides a try-catch block to handle Exception. In the try block, we can write that code that can throw an exception and in the catch, we can write logic to print that exception in logs or write logic to handle it.
    • In catch(), we can pass an argument that is of type Throwable or type of any of its subclass. Like Exception, Error, RuntimeException. We cannot pass any other type like Object, or List, in such case we will get a compile-time error.
    • We can write multiple catch blocks with a single try block, each catch block must handle different exceptions.
    • We cannot write multiple try blocks one after another. Every try block must follow either catch or finally block.
  • 'finally' block
    • During program execution, we may access some resources like files, and DB connection, and when an Exception happens it terminates the program without closing any opened resource.
    • So to handle such a scenario, Java provides a way, whenever an exception happens, the finally block executes, and in the finally block we can write code to close opened resources. So that before the program terminates all resources are closed properly.
    • For Example, sometimes we opened a DB connection and some exceptions happened. Then let's see what happens in the below scenario.
      • If we don't use finally, our application never closes the DB connection, and let's say if our application creates multiple DB connections without closing unused DB connection, it may exhaust the DB connection pool.
      • If we use finally, any exception happens, finally will always execute and it will close the DB connection and then terminate the program.
    • finally, the block must follow try if we are not using catch else catch block.

Some important points to remember,
  • We can write nested try catch finally block within try, catch, and finally block.
  • If we are handling exceptions, try block is a must.
    • Following try, if we write catch block. finally is not mandatory.
    • the catch is also not required if writing finally follows the try block.
  • finally block will always execute, let's understand with example.
  • If we are writing multiple catch blocks then we should handle specific exceptions and then general, else we will get compile time error, or we can say we should first handle subclass then parent classes.

In Java 7 some enhancements were introduced related to Exception handling.

  • we can handle multiple exceptions within a single catch block.
    • Here important point to note we can write multiple exceptions within a single catch block if they are not of the same type, or we should not write parent and subclass within a single catch block.
  • try with resource
    • Using try with resources without the use of finally, Java will help to close resources. In the below example, we use FileReader and BufferedReader to read the file. Try with resource automatically take care auto close for these, even if any exception happens.
    • Try with resource handles auto close for those classes that implement java.io.Closeable interface or java.lang.AutoCloseable interface. Also, the java.io.Closeable interface extends java.lang.AutoCloseable interface.

Java also provides some keywords using which we can throw exceptions in our application code.

  • throw: using throw we can throw an Exception from a method.
  • throws: throws is applied to the method signature which tells the caller, that this method will throw an exception. So caller method can handle it using a try-catch block and can include throws in its method signature.

How to create a custom Exception in Java?

  • We can extend the Exception class
  • If you want to pass some custom message and print it on logs while an exception happens.
    • In that case, we need to write a constructor having a String type message as an argument and the constructor will call the Exception class constructor using the super keyword and pass the message to the Exception class using the super keyword.
  • In case you do not pass it to the Exception class constructor, the error message will not print in logs.
  • As the Throwable class implements a java.io.Serializable interface, all of its subclasses will also be serializable. So it's best practice to define serialVersionUID in our custom Exception class so that it will not create any issue during the serialization and deserialization process.

Comments