Mastering Exception Handling in Java: A Comprehensive Guide

Table of Contents

Exception handling is a critical aspect of Java programming that allows developers to gracefully manage errors and unexpected situations in their code. Java provides a robust mechanism for handling exceptions, which is essential for writing reliable and maintainable applications. In this guide, we’ll explore exception handling in Java in depth, covering various aspects including types of exceptions, try-catch blocks, exception propagation, best practices, and more.

Understanding Exceptions & Exception Handling in Java

Exception

An unexpected, unwanted event that disturbs the normal flow of the program is called an exception. For example, TyredPuncturedException, SleepingException, and FileNotFoundException. It is highly recommended to handle exceptions. The main objective of exception handling is the graceful termination of the program. Exception handling doesn’t mean repairing an exception; rather, it involves providing an alternative way to continue the rest of the program normally. This is the concept of exception handling. For example, if our programming requirement is to read data from a remote file located in London at runtime, and if the London file is not available, our program should not terminate abnormally. Instead, we have to provide some local file to continue the rest of the program normally. This method of defining alternatives is nothing but exception handling.

Java
try {
    // Read data from the remote file located in London
} catch (FileNotFoundException e) {
    // Use a local file and continue the rest of the program normally
}

Runtime Stack Mechanism

For every thread, the JVM will create a runtime stack. Each method call performed by that thread will be stored in the corresponding stack. Each entry in the stack is called a stack frame or activation record. After completing every method call, the corresponding entry is removed from the stack. After completing all method calls, the stack will become empty, and that empty stack will be destroyed by the JVM just before terminating the thread. This is an example of when everything goes well. Next, we will discuss the default exception handling in Java.

Default Exception Handling in Java

Inside a method, if any exception occurs, the method in which it is raised is responsible for creating an exception object by including the following:

  1. Name of the exception.
  2. Description of the exception.
  3. Location at which the exception occurs (Stack Trace).

After creating the exception object, the method hands over that object to the JVM. The JVM checks whether the method contains any exception handling code. If the method doesn’t contain exception handling code, the JVM terminates that method abnormally and removes the corresponding entry from the stack. Then, the JVM identifies the caller method and checks whether the caller method contains handling code or not. If the caller method doesn’t contain handling code, the JVM terminates that caller method also abnormally and removes the corresponding entry from the stack. This process continues until the main method. If the main method also doesn’t contain handling code, the JVM terminates the main method abnormally and removes the corresponding entry from the stack. Then, the JVM hands over the responsibility of exception handling to the default exception handler, which is part of the JVM. The default exception handler prints exception information in the following format and terminates the program abnormally:

Java
----------------------------------------------------------------
Exception in thread "xxx": Name of the exception: Description
                      Stack trace
----------------------------------------------------------------

For example:

Java
class Test {
  public static void main(String[] args) {
    doStuff();
  }
  
  public static void doStuff() {
    doMoreStuff();
  }
  
  public static void doMoreStuff() {
    System.out.println(10 / 0);
  }
}

Runtime Stack:

Java
--------------
doMoreStuff()
--------------
doStuff()
--------------
main()
--------------

Output:

Java
Exception in thread "main": java.lang.ArithmeticException: / by zero
                      at Test.doMoreStuff()
                      at Test.doStuff()
                      at Test.main()

Let’s see one more example,

Java
class Test {
  public static void main(String[] args) {
    doStuff();
    System.out.println(10 / 0);
  }
  
  public static void doStuff() {
    doMoreStuff();
    System.out.println("Hi");
  }
  
  public static void doMoreStuff() {
    System.out.println("Hello");
  }
}

Runtime stack

Java
----------------
----------------
----------------
main()
----------------

Output

Java
Hello
Hi
Exception in thread "main" java.lang.ArithmeticException: / by zero
             at Test.main(Test.java:5)

Note: In a program, if at least one method terminates abnormally, then the program termination is considered abnormal. If all methods terminate normally, then the program termination is considered normal.

Exception Hierarchy

The Throwable class acts as the root for the Java exception hierarchy. The Throwable class defines two child classes:

  1. Exception
  2. Error

Exception: Most of the time, exceptions are caused by our program and are considered recoverable. For example:

Java
try {
    // Read data from the remote file located in London
} catch (FileNotFoundException e) {
    // Use a local file and continue the rest of the program normally
}

Error: Errors are non-recoverable. For example, if an OutOfMemoryError occurs, being a programmer, we cannot do anything, and the program will be terminated abnormally. System admins or server admins are responsible for increasing heap memory.

Visual Representation

Java
Throwable

├── Exception
│   │
│   ├── RuntimeException
│   │   │
│   │   ├── ArithmeticException
│   │   ├── NullPointerException
│   │   ├── ClassCastException
│   │   ├── IndexOutOfBoundsException
│   │   │   │
│   │   │   ├── ArrayIndexOutOfBoundsException
│   │   │   └── StringIndexOutOfBoundsException
│   │   └── IllegalArgumentException
│   │       └── NumberFormatException
│   │
│   ├── IOException
│   │   │
│   │   ├── EOFException
│   │   ├── FileNotFoundException
│   │   └── InterruptedIOException
│   │
│   └── ServletException

└── Error

    ├── VirtualMachineError
    │   │
    │   ├── StackOverflowError
    │   └── OutOfMemoryError

    ├── AssertionError
    └── ExceptionInInitializerError

Types of Exceptions

Checked Exceptions

Checked exceptions are the exceptions that are checked at compile-time. These exceptions are subclasses of Exception but not subclasses of RuntimeException. Examples of checked exceptions include IOException, SQLException, and FileNotFoundException. It is mandatory for a method to either handle these exceptions using a try-catch block or declare them using the throws keyword in the method signature.

Unchecked Exceptions

Unchecked exceptions are the exceptions that are not checked at compile-time. They are subclasses of RuntimeException. Examples of unchecked exceptions include NullPointerException, ArrayIndexOutOfBoundsException, and ArithmeticException. It is not mandatory to handle unchecked exceptions explicitly, although it’s good practice to do so.

Checked Exception vs Unchecked Exception

Checked exceptions are those that are checked by the compiler for smooth program execution. Examples include HallTicketMissingException, PenNotWorkingException, and FileNotFoundException. If there is a chance of raising a checked exception in our program, we must handle it either by using try-catch blocks or by declaring it with the throws keyword; otherwise, we will get a compile-time error.

Unchecked exceptions, on the other hand, are not checked by the compiler for whether the programmer handles them or not. Examples include ArithmeticException and BomBlastException.

Note: Regardless of whether an exception is checked or unchecked, every exception occurs at runtime only; there is no chance of an exception occurring at compile time.

Note: RuntimeException and its child classes, as well as Error and its child classes, are unchecked. All other exceptions are checked.

Fully Checked vs Partially Checked

A checked exception is fully checked if all its child classes are also checked, e.g., IOException and InterruptedException. A checked exception is partially checked if only some of its child classes are unchecked, such as Exception and Throwable.

Exception Behavior Description:

  • IOException: Checked (fully)
  • RuntimeException: Unchecked
  • InterruptedException: Checked (fully)
  • Error: Unchecked
  • Throwable: Checked (partially)
  • ArithmeticException: Unchecked
  • NullPointerException: Unchecked
  • Exception: Checked (partially)
  • FileNotFoundException: Checked (fully)

Customized Exception Handling by Using Try-Catch

It is highly recommended to handle exceptions. The code that may raise an exception is called risky code, and we have to define that code inside the try block. The corresponding handling code should be defined inside the catch block.

Java
try {
    // Risky code
} catch(Exception e) {
    // Handling code
}

Without Try-Catch

Java
class Test {
    public static void main(String[] args) {
        System.out.println("stmt 1");
        System.out.println(10/0);
        System.out.println("stmt 3");
    }
}

Output:

Java
stmt 1
RE: ArithmeticException: / by zero

Abnormal Termination

With Try-Catch

Java
class Test {
    public static void main(String[] args) {
        System.out.println("stmt 1");
        try {   
            System.out.println(10/0);
        } catch(ArithmeticException e) {
            System.out.println(10/2);
        }
        System.out.println("stmt 3");
    }
}

Output:

Java
Stmt 1
5
Stmt 3

Normal Termination

In the first example without try-catch, the program encounters an ArithmeticException due to division by zero, leading to abnormal termination. In the second example with try-catch, the exception is caught, and the program continues executing the remaining statements, resulting in normal termination.

Control Flow in Try-Catch

Java
try {
    stmt1;
    stmt2;
    stmt3;
} catch(Exception e) {
    stmt4;
}
stmt5;

Case 1: If there is no exception, then output will be (1, 2, 3, 5, Normal Termination).

Case 2: If an exception is raised at statement 2 and the corresponding catch block is matched, then the output will be (1, 4, 5, Normal Termination).

Case 3: If an exception is raised at statement 2 and the corresponding catch block is not matched, then the output will be (1, Abnormal Termination).

Case 4: If an exception is raised at statement 4 or statement 5, then it is always an abnormal termination.

Note:

  1. Within the try block, if an exception is raised anywhere, then the rest of the try block won’t be executed, even if we handle that exception. Therefore, within a try block, we should only place risky code, and the length of the try block should be as short as possible.
  2. In addition to the try block, there may be a chance of an exception occurring inside catch and finally blocks. If any statement outside the try block raises an exception, then it always results in an abnormal termination.

Methods to Print Exception Information

The Throwable class defines the following methods to print exception information:

printStackTrace(): This method prints the exception’s name, description, and stack trace, showing the sequence of method calls that led to the exception.

Printable Format:

Java
NameOfException: Description
Stack Trace

toString(): Returns a string representation of the exception, including its name and description.

Java
Name of Exception: Description

getMessage(): Returns the description of the exception.

Java
Description

Example Program:

Java
class Test {
    public static void main(String[] args) {
        try {
            System.out.println(10/0);
        } catch(ArithmeticException e) {
            e.printStackTrace(); // java.lang.ArithmeticException: / by zero at Test.main(Test.java:4)
            System.out.println(e); // java.lang.ArithmeticException: / by zero
            System.out.println(e.toString()); // java.lang.ArithmeticException: / by zero
            System.out.println(e.getMessage()); // / by zero
        }
    }
}

Note: Internally, the default exception handler will use printStackTrace() to print stack trace information to the console.

try with multiple catch blocks

The way of handling an exception varies from exception to exception; hence, for every exception type, it is highly recommended to use a separate catch block. Using try with multiple catch blocks is always possible and recommended.

Worst Programming Practice:

Java
try {
    // Risky code
} catch(Exception e) {
    // For all exceptions, use this single catch block
}

Best Programming Practice:

Java
try {
    // Risky Code
} catch(ArithmeticException e) {
    // Perform alternative arithmetic operation
} catch(SqlException e) {
    // Use MySQL DB instead of Oracle DB
} catch(FileNotFoundException e) {
    // Use a local file instead of a remote file
} catch(Exception e) {
    // Default exception handling
}

In the best programming practice approach, each specific exception type is caught and handled appropriately, providing a more tailored and robust error-handling mechanism.

Important Loopholes

Some important loopholes in exception handling:

1. Order of catch blocks matters

If a try with multiple catch blocks is present, then the order of catch blocks is very important. We have to take the child exceptions first and then the parent exceptions; otherwise, we will get a compile-time error saying “Exception XXX has already been caught”.

Java
try {
    // Risky code
} catch (Exception e) {
    // Parent exception catch block
} catch (ArithmeticException e) {
    // Child exception catch block
}

// CE: Exception java.lang.ArithmeticException has already been caught

The correct code order should be:

Java
try {
    // Risky code
} catch (ArithmeticException e) {
    // Child exception catch block
} catch (Exception e) {
    // Parent exception catch block
}
2. Cannot declare duplicate catch blocks

We cannot declare two catch blocks for the same exception; otherwise, we will get a compile-time error.

Java
try {
    // Risky code
} catch (ArithmeticException e) {
    // Catch block for ArithmeticException
} catch (ArithmeticException e) {
    // Another catch block for ArithmeticException (Duplicate)
}

// CE: Exception java.lang.ArithmeticException has already been caught

The correct code should be:

Java
try {
    // Risky code
} catch (ArithmeticException e) {
    // Catch block for ArithmeticException
} catch (Exception e) {
    // Catch block for other exceptions
}

These loopholes highlight the importance of careful handling and structuring of catch blocks to ensure effective exception management in Java programs.

Difference between final, finally, and finalize

final

  • final is a modifier applicable for classes, methods, and variables.
  • If a class is declared as final, then we can’t extend that class, meaning we can’t create a child class for that class. Inheritance is not possible for final classes.
  • If a method is final, then we can’t override that method in the child class. If a variable is declared as final, then we can’t perform reassignment for that variable.

finally

  • finally is a block always associated with try-catch to maintain clean-up code.
  • The speciality of the finally block is that it will be executed always, irrespective of whether an exception is raised or not, and whether it is handled or not.
  • It is responsible for performing clean-up activities related to the try block. Any resources opened as part of the try block will be closed inside the finally block.
Java
try {
    // Risky code
} catch (Exception e) {
    // Handling code
} finally {
    // Clean-up code
}

finalize()

  • finalize() is a method always invoked by the garbage collector just before destroying an object to perform clean-up activities.
  • Once the finalize() method completes, immediately the garbage collector destroys that object.
  • It is responsible for performing clean-up activities related to the object. Any resources associated with the object will be deallocated before destroying the object using the finalize() method.

Note:

The finally block is responsible for performing clean-up activities related to the try block. Any resources opened as part of the try block will be closed inside the finally block. It ensures that resources are released regardless of whether an exception occurs or not.

On the other hand, the finalize() method is responsible for performing clean-up activities related to the object. It is invoked by the garbage collector just before destroying an object. Any resources associated with the object will be deallocated before destroying the object using the finalize() method. This method is used to release resources held by the object and perform any necessary clean-up actions before the object is removed from memory.

In short, final is used to denote immutability or prevent extension/overriding, finally is used for try-catch cleanup, and finalize() is used for object-specific cleanup just before garbage collection. Each serves a distinct purpose in Java programming.

Various Possible Combinations of Try-Catch-Finally

Rules:

  1. Order is Important: In try-catch-finally, the order is important, the order should be try, catch, and then finally.
  2. Compulsory Usage: Whenever we write try, it’s compulsory to include either catch or finally; otherwise, we will get a compile-time error (try without catch or finally is invalid).
  3. Compulsory Try Block for Catch: Whenever we write a catch block, a try block must be present; catch without try is invalid.
  4. Compulsory Try Block for Finally: Whenever we write a finally block, a try block should be present; finally without try is invalid.
  5. Nesting of Try-Catch-Finally: Inside try-catch-finally blocks, we can nest additional try-catch-finally blocks; nesting of try-catch-finally is allowed.
  6. Curly Braces Requirement: Curly braces ({}) are mandatory for try-catch-finally blocks.

Examples:

Valid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
}

Valid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} catch(Y e) {
  // exception handling code
}

Invalid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} catch(X e) {
  // exception handling code
}

// CE: exception X has already been caught

Valid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} finally {
  // cleanup code
}

Valid:

Java
try {
  // code
} finally {
  // cleanup code
}

Valid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} try {
  // code
} catch(Y e) {
  // exception handling code
}

Valid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} try {
  // code
} finally {
  // cleanup code
}

Invalid:

Java
try {
  // code
}
// CE: try without catch (or) finally

Invalid:

Java
catch(X e) {
  // exception handling code
}
// CE: catch without try

Invalid:

Java
finally {
  // cleanup code
}
// CE: finally without try

Invalid:

Java
try {
  // code
} finally {
  // cleanup code
} catch(X e) {
  // exception handling code
}
// CE: catch without try 

Invalid:

Java
try {
  // code
} System.out.println("Hello");
catch(X e) {
  // exception handling code
}
// CE1: try without catch or finally 
// CE2: catch without try

Invalid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} System.out.println("Hello");
catch(Y e) {
  // exception handling code
}
// CE: catch without try

Invalid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} System.out.println("Hello");
finally {
  // cleanup code
}
// CE: finally without try

Valid:

Java
try {
  try {
    // code
  } catch(X e) {
    // exception handling code
  }
} catch(X e) {
}

Invalid:

Java
try {
  try {
   // code
  }
} catch(X e) {
  // exception handling code
}
// CE: try without catch or finally 

Valid:

Java
try {
  try {
    // code
  } finally {
    // cleanup code
  }
} catch(X e) {
  // exception handling code
}

Valid:

Java
try {
  // code
} catch(X e) {
  try {
  } finally {
  }
}

Invalid:

Java
try {
  // code
} catch(X e) {
 finally {
 }
}
// CE: finally without try

Valid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} finally {
  try {
  } catch(X e) {
  }
}

Invalid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} finally {
  finally {
  }
}
// CE: finally without try

Invalid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} finally {
}
finally {
}
// CE: finally without try

Invalid:

Java
try
 System.out.println("try");
catch(X e)
 System.out.println("catch");
finally
{
}
// CE: finally without try | Syntax error, insert "}" to complete Block

Invalid:

Java
try{
  // code
}
catch(X e)
System.out.println("catch");
finally{
  // cleanup code
}
// CE: finally without try | Syntax error, insert "{" to complete Block

Invalid:

Java
try {
  // code
} catch(X e) {
} finally
System.out.println("finally");

// CE: finally without try | Syntax error on token "System", invalid Expression

Valid:

Java
try {
    try {
        // code
    } catch(Exception e) {
        // inner catch block
    } finally {
        // inner finally block
    }
} catch(Exception e) {
    // outer catch block
} finally {
    // outer finally block
}

These examples demonstrate the various valid and invalid combinations of try-catch-finally blocks according to the specified rules.

Throw (throw keyword)

The throw keyword is used to manually create and throw an exception object. Sometimes, we need to generate exception objects explicitly and hand them over to the JVM

Java
throw new ArithmeticException("/ by zero");

Here, we are creating an ArithmeticException object explicitly and throwing it using the throw keyword.

The primary purpose of the throw keyword is to hand over our created exception object to the JVM manually.

The result of the following two programs is exactly the same:

Without throw keyword:

Java
class Test {
    public static void main(String[] args) {
        System.out.println(10/0);
    }
}

Output:

Java
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Test.main(Test.java:3)

In this case, the main method is responsible for causing the ArithmeticException and implicitly hands over the exception object to the JVM.

With throw keyword:

Java
class Test {
    public static void main(String[] args) {
        throw new ArithmeticException("/ by zero");
    }
}

Output:

Java
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Test.main(Test.java:3)

In this case, the programmer is explicitly creating an AE (ArithmeticException) object and throwing it using the throw keyword. This means the programmer is manually handing over the exception object to the JVM.

Note: The best use of the throw keyword is for user-defined or customized exceptions.

Java
class Test {
    static AE e = new AE();
    public static void main(String[] args) {
        throw e;
    }
}


//Output - Runtime Error: AE

In this case, the throw keyword is used to throw a reference to an exception object e (AE object)

Java
class Test {
    static AE e;
    public static void main(String[] args) {
        throw e;
    }
}


//Output - Runtime Error: NullPointerException

Here, e is a static member variable of type AE, but it is not initialized explicitly. Therefore, it defaults to null. When attempting to throw e, a NullPointerException occurs.

So, the throw keyword is used to explicitly throw exceptions, which can be either built-in or user-defined. However, it’s crucial to ensure that the exception object being thrown is properly initialized to avoid runtime errors like NullPointerException.

Unreachable Statement after throw:

After a throw statement, any code that follows it becomes unreachable because the exception is immediately thrown, and the program flow doesn’t continue beyond that point.

Java
class Test {
    public static void main(String[] args) {
        System.out.println(10/0);
        System.out.println("Hello");
    }
}


//Runtime Error: AE: / by zero

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Test.main(Test.java:3)

Unreachable Statement Error:

If there are statements after a throw statement, they are considered unreachable, and the compiler raises a compile-time error indicating “unreachable statement”.

Java
class Test {
    public static void main(String[] args) {
        throw new AE("/ by zero");
        System.out.println("Hello");
    }
}


//Compile Error: unreachable statement

Usage of throw with Non-Throwable Types:

The throw keyword is only applicable for throwable types (subclasses of Throwable). If we attempt to use it with non-throwable types, it results in a compile-time error due to incompatible types.

Java
class Test {
    public static void main(String[] args) {
        throw new Test();
    }
}


//Compile Error: incompatible types, found: Test, required: java.lang.Throwable

Compilation Error: incompatible types
    found:    Test
    required: java.lang.Throwable

Inheriting from RuntimeException:

If a class extends RuntimeException, it becomes a throwable type, and instances of this class can be thrown using the throw keyword.

Java
class Test extends RuntimeException {
    public static void main(String[] args) {
        throw new Test();
    }
}


// runtime exception

Exception in thread "main" Test
    at Test.main(Test.java:3)

In this example, Test is a subclass of RuntimeException, so it can be thrown without any compilation errors. When executed, it results in a runtime exception.

Throws (throws keyword)

In Java, if there is a possibility of a checked exception being thrown within a method, the method must either handle the exception using a try-catch block or declare that it throws the exception using the throws keyword in its method signature.

Java
import java.io.*;

class Test {
    public static void main(String[] args) {
        PrintWriter pw = new PrintWriter("abc.txt");
        pw.println("Hello");
    }
}


//Compilation Error: unreported exception java.io.FileNotFoundException; must be caught or declared to be thrown

Here, the PrintWriter constructor throws a checked exception FileNotFoundException, which is not handled. Hence, a compilation error occurs.

To handle this error, you can either use a try-catch block to handle the exception:

Java
import java.io.*;

class Test {
    public static void main(String[] args) {
        try {
            PrintWriter pw = new PrintWriter("abc.txt");
            pw.println("Hello");
        } catch (FileNotFoundException e) {
            // Handle the exception
            e.printStackTrace();
        }
    }
}

Or we can declare that the method throws the exception using the throws keyword:

Java
import java.io.*;

class Test {
    public static void main(String[] args) throws FileNotFoundException {
        PrintWriter pw = new PrintWriter("abc.txt");
        pw.println("Hello");
    }
}

Using the throws keyword delegates the responsibility of handling the exception to the caller of the method.

Note: It’s recommended to handle exceptions using try-catch blocks where possible, as using throws may propagate exceptions up the call stack without handling them properly.

Let’s take one more example,

Java
class Test {
    public static void main(String[] args) {
        Thread.sleep(10000);
    }
}

//CE: unreported exception java.lang.InterruptedException must be caught or declared to be thrown

The Thread.sleep() method may throw an InterruptedException, which is a checked exception. Since this exception is not handled within the main method, nor is it declared using the throws keyword in the method signature, the compiler raises a compilation error:

Here, we can declare that the method throws the exception using the throws keyword:

Java
class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(10000);
    }
}

Using the throws keyword delegates the responsibility of handling the exception to the caller of the method. However, it’s important to handle exceptions properly to prevent unexpected behavior in the program.

By Using throws Keyword

The throws keyword in Java is used in method declarations to indicate that a method might throw certain exceptions during its execution. This alerts the caller of the method that they need to handle or propagate these exceptions.

Java
class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(10000);
    }
}

In this example, the main method uses the throws keyword to declare that it might throw an InterruptedException during its execution. This means that if the sleep() method within main throws an InterruptedException, the caller of main (in this case, the JVM) must handle it.

Similarly, if a method calls another method that declares a checked exception using throws, the calling method must either handle that exception or re-throw it using throws.

Java
class Test {
    public static void main(String[] args) throws IE {
        doStuff();
    }

    public static void doStuff() throws IE {
        doMoreStuff();
    }

    public static void doMoreStuff() throws IE {
        Thread.sleep(10000);
    }
}

In this example, the doStuff method declares that it might throw an IE (hypothetical checked exception). Consequently, since main calls doStuff, it must either handle IE or declare that it throws it, as it propagates up the call stack from doMoreStuff().

However, it’s important to note that using throws doesn’t prevent abnormal termination of the program; it’s merely a way to indicate potential exceptions that might be thrown. Additionally, throws is only required for checked exceptions; there’s no impact or requirement for unchecked exceptions.

throws clause

  1. The throws clause can be used to delegate the responsibility of exception handling to the caller, whether it is a method or the JVM.
  2. It is required only for checked exceptions; the usage of the throws keyword for unchecked exceptions has no impact.
  3. It is required only to convince the compiler; the usage of throws does not prevent the abnormal termination of the program.

Note: It is recommended to use try-catch blocks over the throws keyword.

Case 1: We can use the throws keyword for methods and constructors, but not for classes.

Java
class Test throws Exception   //invalid 
{
    Test() throws Exception   // valid 
    {
    }
    public void m1() throws Exception  //valid  
    {
    }
}

Case 2: We can use the throws keyword only for throwable types. If we try to use it for normal Java classes, we will get a compile-time error saying ‘incompatible types.

Java
class Test
{
    public void m1() throws Test
    {
    }
}

// Compile Error: Incompatible types
// Found: Test
// Required: java.lang.Throwable

Case 3:

Java
class Test
{
    public static void main(String[] args)
    {
        throw new Exception(); // Exception is checked
    }
}

//Compile Error: Unreported exception java.lang.Exception; must be caught or declared to be thrown
Java
class Test
{
    public static void main(String[] args)
    {
        throw new Error(); // Error is unchecked
    }
}

//Runtime Error: 
Exception in thread "main" java.lang.Error
at Test.main(Test.java:lineNumber)

Case 4: Within a try block, if there is no chance of raising an exception, then we can’t write a catch block for that exception; otherwise, we will get a compilation error: ‘Exception XXX is never thrown in the body of the corresponding try statement.’ However, this rule is applicable only for fully checked exceptions.

Java
class Test
{
    public static void main(String[] args)
    {
        try 
        {
            System.out.println("Hello");
        }
        catch(AE e)   // AE is unchecked exception 
        {
        }
    }
}


Examples and Outputs

Java
class Test
{
    public static void main(String[] args)
    {
        try 
        {
            System.out.println("Hello");
        }
        catch(AE e)   // AE is unchecked exception 
        {
        }
    }
}

/////////////////////////////////////////////////

class Test 
{
    public static void main(String[] args)
    {
        try
        {
            System.out.println("hello");
        }
        catch(Exception e)   // partially checked 
        {
        }
    }
}

//////////////////////////////////////////////////////////////////

class Test
{
    public static void m(String[] args)
    {
        try
        {
            System.out.println("Hello");
        }
        catch(Error e)  // unchecked
        {
        }
    }
}

// Output - hello (in all three cases)

Java
import java.io.*;
class Test
{
    public static void main(String[] args)
    {
        try
        {
            System.out.println("Hello");
        }
        catch(IOException e)   // Fully checked
        {
        }
    }
}

// Compile Error: Exception java.io.IOException is never thrown in the body of the corresponding try statement


////////////////////////////////////////////////////////////////////////////////////////

class Test
{
    public static void main(String[] args)
    {
        try
        {
            System.out.println("Hello");
        }
        catch(InterruptedException e)  // fully checked
        {
        }
    }
}

// Compile Error: Exception java.lang.InterruptedException is never thrown in the body of the corresponding try statement

Customized or user-defined exception

Sometimes, to meet programming requirements, we can define our own exceptions. Such types of exceptions are called customized or user-defined exceptions, e.g., TooYoungException, TooOldException, InsufficientFundsException, etc.

Java
class TooYoungException extends RuntimeException {
    TooYoungException(String s) {
        super(s);
    }
}

class TooOldException extends RuntimeException {
    TooOldException(String s) {
        super(s);
    }
}

class CustomizedExceptionDemo {
    public static void main(String[] args) {
        int age = Integer.parseInt(args[0]);
        if (age > 60) {
            throw new TooYoungException("Please wait some more time; definitely, you will get the best match.");
        } else if (age < 18) {
            throw new TooOldException("Your age already crossed the marriage age, and there is no chance of getting married.");
        } else {
            System.out.println("You will get match details soon by email!");
        }
    }
}

Note:

  1. The throw keyword is best suitable for user-defined or customized exceptions but not for predefined exceptions.
  2. It is highly recommended to define customized exceptions as unchecked, i.e., we have to extend RuntimeException but not Exception.

In the example, why is super(s) required?

  • super(s) is required to pass the description message to the superclass constructor, making it available to the default exception handler.

Exception Handling CheatSheet

Exception handling keyword summary

  1. try –> Used to encapsulate risky code.
  2. catch –> Used to handle exceptions.
  3. finally –> Used to execute cleanup code.
  4. throw –> Used to manually pass our created exception object to the JVM.
  5. throws –> Used to delegate the responsibility of exception handling to the caller.

Various possible compiler errors in exception handling

  1. “Unreported exception XXX; must be caught or declared to be thrown.”
  2. “Exception XXX has already been caught.”
  3. “Exception XXX is never thrown in the body of the corresponding try statement.”
  4. “Unreachable statement.”
  5. “Incompatible types: found: Test, required: java.lang.Throwable.”
  6. “Try without catch or finally.”
  7. “Catch without try.”
  8. “Finally without try.”

Top 10 Exceptions in Java

Exceptions in Java are divided into two categories based on the entity raising them:

  1. JVM Exception: These exceptions are raised automatically by the JVM whenever particular events occur. Examples include ArithmeticException and NullPointerException.
  2. Programmatic Exception: These exceptions are raised explicitly, either by the programmer or by API developers, to indicate that something has gone wrong. Examples include TooOldException and IllegalArgumentException.

Top Ten Exceptions

1. ArrayIndexOutOfBoundsException: This exception is a subclass of RuntimeException and hence is unchecked. It is automatically raised by the JVM when attempting to access an array element with an index that is out of the array’s range. For example:

Java
int[] x = new int[4]; // Indices 0 to 3
System.out.println(x[0]); // 0
System.out.println(x[10]); // Throws ArrayIndexOutOfBoundsException
System.out.println(x[-10]); // Throws ArrayIndexOutOfBoundsException

2. NullPointerException: This exception is a subclass of RuntimeException and hence is unchecked. It is automatically raised by the JVM when attempting to perform any operation on a null object. For example:

Java
String s = null;
System.out.println(s.length()); // Throws NullPointerException

3. ClassCastException: This exception is a subclass of RuntimeException and hence is unchecked. It is automatically raised by the JVM when attempting to type cast a parent object to a child type.

Valid example:

Java
String s = new String("durga");
Object o = (Object)s;

Invalid example:

Java
Object o = new Object();
String s = (String)o;
// Throws ClassCastException

4. StackOverflowError: This error is a subclass of Error and hence is unchecked. It is automatically raised by the JVM when attempting to perform a recursive method call.

Java
class Test {
    public void m1() {
        m2();
    } 

    public void m2() {
        m1();
    }

    public static void main(String[] args) {
        Test t = new Test();
        t.m1();
    }
}

The recursive method calls in this code snippet would lead to a StackOverflowError:

Java
       .
       .
       .
       .
------------------
      m2()
------------------
      m1()
------------------
      m2()
------------------
      m1()
------------------
     main()
------------------



// RE : StackOverFlowError

5. NoClassDefFoundError: This error is a subclass of Error and hence is unchecked. It is automatically raised by the JVM when it is unable to find the required .class file. For example, if the Test.class file is not available, then attempting to run java Test will result in a runtime exception with the message: “NoClassDefFoundError: Test”.

6. ExceptionInInitializerError: This error is a subclass of Error and hence is unchecked. It is automatically raised by the JVM if any exception occurs while executing static variable assignments or static blocks.

Java
class Test {
    static int x = 10/0;
}

// ExceptionInInitializerError 
         caused by java.lang.ArithmeticException: / by zero

Another example:

Java
class Test {
    static {
        String s = null;
        System.out.println(s.length());
    }
}


// ExceptionInInitializerError caused by: java.lang.NullPointerException

7. IllegalArgumentException: This exception is a subclass of RuntimeException and hence is unchecked. It is raised explicitly either by the programmer or by API developers to indicate that a method has been invoked with an illegal argument. For example, suppose the valid range of thread priority is 1 to 10. If we attempt to set the priority with any other value, then we will get a RuntimeException with the message “IllegalArgumentException”

Java
Thread t = new Thread();
t.setPriority(7); // Valid
t.setPriority(15); // Throws IllegalArgumentException

8. NumberFormatException: This exception is a direct subclass of IllegalArgumentException and indirectly a subclass of RuntimeException, making it unchecked. It is raised explicitly either by the programmer or API developers to indicate that a conversion from a string to a number has been attempted, but the string is not properly formatted.

Java
int i = Integer.parseInt("10"); // Valid
int i = Integer.parseInt("ten"); // Throws NumberFormatException

9. IllegalStateException: This exception is a subclass of RuntimeException and hence is unchecked. It is raised explicitly either by the programmer or API developers to indicate that a method has been invoked at the wrong time. For example, after starting a thread, we are not allowed to restart the same thread once again; otherwise, we will get a RuntimeException with the message “IllegalThreadStateException”.

Java
// Example of IllegalThreadStateException
Thread t = new Thread();
t.start();
t.start(); // Throws IllegalStateException

10. AssertionError: This exception is a subclass of Error and hence is unchecked. It is raised explicitly by the programmer or by API developers to indicate that an assert statement fails. For example, if the condition x > 10 is not met, then we will get a RuntimeException with the message “AssertionError”.

Java
// Example of AssertionError
int x = 5;
assert(x > 10); // Throws AssertionError

Raised by:

Raised automatically by the JVM and hence these are JVM exceptions:
  • ArrayIndexOutOfBoundsException
  • NullPointerException
  • ClassCastException
  • StackOverflowError
  • NoClassDefFoundError
  • ExceptionInInitializerError
Raised explicitly either by the programmer or by API developer and hence these are programmatic exceptions:
  • IllegalArgumentException
  • NumberFormatException
  • IllegalStateException
  • AssertionError

Java 1.7v Enhancements in Exception Handling

As part of version 1.7, two concepts were introduced for exception handling:

  1. Try-with-Resources
  2. Multi-catch block

Until version 1.6, it was highly recommended to write a finally block to close resources that were opened as part of a try block:

Java
try {
    br = new BufferedReader(new FileReader("input.txt"));
    // Use br based on our requirement
} catch (IOException e) {
    // Handling code
} finally {
    if (br != null) {
        br.close();
    }
}

The problems with the above approach are:

  • The programmer is required to close resources inside the finally block, increasing the complexity of programming.
  • The finally block is mandatory, which increases the length of the code and reduces readability.

To overcome these problems, the developers introduced try-with-resources in version 1.7.

The main advantage of try-with-resources is that whatever resources we open as part of the try block will be closed automatically once control reaches the end of the try block, either normally or abnormally. Therefore, explicit closure is not required, reducing the complexity of programming. Additionally, there’s no need to write a finally block, which reduces the length of the code and improves readability.

1.7v Try with Resources

In version 1.7, the try-with-resources statement was introduced:

Java
try (BR br = new BR(new FR("input.txt"))) {
    // Use br based on our requirement 
    // br will be closed automatically once control reaches the end of the try block, either normally 
    // or abnormally, and we are not responsible for closing it explicitly
} catch (IOException e) {
    // Handling code
}

In this syntax, resources declared within the parentheses after the try keyword are automatically closed when the try block exits, whether normally or due to an exception. This feature simplifies resource management and improves code readability.

Try with Multiple Resources (1.7v)

In version 1.7, it became possible to declare and manage multiple resources in a single try-with-resources statement. These resources should be separated with semicolons:

Java
try (R1; R2; R3) {
    // Code block
}

For example:

Java
try (FileWriter fw = new FileWriter("Output.txt"); FileReader fr = new FileReader("input.txt")) {
    // Code block
}

All resources specified within the try-with-resources statement must implement the AutoCloseable interface. A resource is considered AutoCloseable if the corresponding class implements the java.lang.AutoCloseable interface.

It’s worth noting that many I/O-related, database-related, and network-related resources already implement the AutoCloseable interface. As programmers, we are not required to do anything specific other than being aware of this feature.

AutoCloseable Interface in Java 1.7v

The AutoCloseable interface was introduced in version 1.7 and contains only one method: close().

All resource variables declared within a try-with-resources statement are implicitly final. Therefore, within a try block, we cannot perform reassignment to these variables; otherwise, we will encounter a compilation error. For example:

Java
import java.io.*;

class TryWithResources {
    public static void main(String[] args) throws Exception {
        try (BufferedReader br = new BufferedReader(new FileReader("input.txt"))) {
            br = new BufferedReader(new FileReader("output.txt")); // Compilation Error
        }
    }
}

Compilation Error:

Java
AutoCloseable resource 'br' may not be assigned.
BufferedReader br = new BufferedReader(new FileReader("Output.txt"));

Until version 1.6, the try block should be associated with either catch or finally. However, from version 1.7 onward, we can use try-with-resources without explicitly specifying catch or finally blocks:

Java
try (R) {
    // Code block
}

The main advantage of try-with-resources is that we are not required to write a finally block explicitly because we do not need to close resources explicitly. Therefore, until version 1.6, the finally block was considered essential, but from version 1.7 onward, it becomes unnecessary.

Multi-Catch Block

Until version 1.6, even if multiple different exceptions required the same handling code, separate catch blocks had to be written for each exception type. This approach increased the length of the code and reduced readability:

Java
try {
    // Code block
} catch (AE e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
} catch (NPE e) {
    System.out.println(e.getMessage());
} catch (InterruptedException e) {
    System.out.println(e.getMessage());
}

To overcome this problem, the developers introduced the multi-catch block in version 1.7. With multi-catch blocks, a single catch block can handle multiple different types of exceptions:

Java
try {
    // Code block
} catch (AE | IOException e) {
    e.printStackTrace();
} catch (NPE | InterruptedException e) {
    System.out.println(e.getMessage());
}

The main advantage of this approach is that the length of the code is reduced, and readability is improved.

Java
class MultiCatchBlock {
    public static void main(String[] args) {
        try {
            System.out.println(10/0);
            String s = null;
            System.out.println(s.length());
        } catch (AE | NullPointerException e) {
            System.out.println(e);
        }
    }
}

In the above example, whether the raised exception is AE or NPE, the same catch block can handle it.

In a multi-catch block, there should not be any relation between exception types, such as child-to-parent, parent-to-child, or same type; otherwise, a compilation error will occur.

Java
try {
    // Code block
} catch (AE | Exception e) { 
    e.printStackTrace();
}

//Compilation Error: Alternatives in a multi-catch statement cannot be related by subclassing.

Exception Propagation: Inside a method, if an exception is raised and not handled, the handling will be propagated to the caller. Then, the caller method is responsible for handling the exception. This process is called exception propagation.

Re-throwing Exception: This approach is used to convert one exception to another exception.

Java
try {
    System.out.println(1 / 0);
} catch (AE e) {
    throw new NullPointerException();
}

Conclusion

Exception handling is an essential aspect of Java programming that enables developers to write robust and reliable code. By understanding the different types of exceptions, using try-catch blocks effectively, and following best practices for exception handling, you can create Java applications that gracefully handle errors and unexpected situations. Remember to handle exceptions appropriately, provide informative error messages, and clean up resources properly to ensure the reliability and maintainability of your code.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!