Page Content

Tutorials

What is a Nested Block in Java? & Create a Custom Exception?

Nested Block in Java

You can stack one try block inside another. This is helpful for handling different types of mistakes in different ways; for example, inner try blocks handle less serious problems, while outer try blocks may catch more significant ones.

If an exception is created inside an inner try block and its corresponding catch handler fails to capture it, it is passed to the outer try block. In order to find a match, the outer try block then checks its own catch handlers. This procedure keeps going up the chain of nested try blocks until either all of the nested try blocks have been used up or a catch statement fits the exception type. The exception will be handled by the default exception handler of the Java runtime system if no catch matches.

Here’s an example of nested try blocks:

public class NestedTryDemo {
    public static void main(String[] args) {
        try { // Outer try block
            int[] outerArray = {1, 2, 3};
            System.out.println("Outer block: Accessing outerArray: " + outerArray);
            try { // Inner try block
                int[] innerArray = new int[44];
                System.out.println("Inner block: Trying to divide by zero...");
                int divisionResult = 10 / 0; // Generates ArithmeticException
                System.out.println("Inner block: Division result: " + divisionResult); // This won't be displayed
            } catch (ArrayIndexOutOfBoundsException innerExc) {
                // This catch only handles ArrayIndexOutOfBoundsException
                System.out.println("Inner Catch: Caught ArrayIndexOutOfBoundsException: " + innerExc.getMessage());
            }
            System.out.println("Outer block: After inner try-catch."); // This won't be displayed if inner throws ArithmeticException
        } catch (ArithmeticException outerExc) {
            // This catch handles ArithmeticException propagated from inner try
            System.out.println("Outer Catch: Caught ArithmeticException: " + outerExc.getMessage());
        } catch (Exception genericExc) {
            // Generic catch for any other unexpected exceptions
            System.out.println("Outer Catch: Caught a generic exception: " + genericExc.getMessage());
        } finally {
            System.out.println("Finally block: Always executed.");
        }
    }
}

Output:

Outer block: Accessing outerArray: 1
Inner block: Trying to divide by zero...
Outer Catch: Caught ArithmeticException: / by zero
Finally block: Always executed.

The inner catch (for ArrayIndexOutOfBoundsException) in this example does not capture the ArithmeticException thrown in the inner try section. The catch (ArithmeticException outerExc) block successfully captures it once it propagates to the outer try block.

Built-in Exceptions

All exceptions in Java are represented by classes derived from the Throwable class, which sits at the top of the exception hierarchy. Throwable has two direct subclasses: Error and Exception.

  • Error: These consist of major issues like OutOfMemoryError or StackOverflowError that arise in the Java Virtual Machine (JVM) itself. Programmers normally have no control over errors, and applications typically don’t address them.
  • Exception: Exceptions are errors that arise from program activity and are typically handled by your program. A significant subclass of the Exception class is called RuntimeException.

Moreover, Java divides exceptions into two primary groups:

  1. Unchecked Exceptions: Subclasses of RuntimeException are known as Unchecked Exceptions. If a method handles or declares these exceptions in a throws list, the compiler doesn’t verify it. Usually, these are unexpected occurrences that are frequently caused by program defects.
    • Examples include: ArithmeticException (e.g., division by zero), NullPointerException (invalid use of a null reference), ArrayIndexOutOfBoundsException (array index out of bounds), InputMismatchException, and NumberFormatException.
  2. Checked Exceptions: These are subclasses of Exception but not RuntimeException. An application should be able to handle these exceptions, which stand for expected events. The throws keyword must be used to declare a method that can throw a checked exception but does not handle it; otherwise, a compile-time error will occur.
    • Examples include: IOException (I/O error), FileNotFoundExceptionSQLException, and InterruptedException.

Let’s look at an example demonstrating ArithmeticException and ArrayIndexOutOfBoundsException:

public class BuiltInExceptionsDemo {
    public static void main(String[] args) {
        // Demonstrate ArithmeticException
        try {
            int result = 10 / 0; // This will cause an ArithmeticException
            System.out.println("Result: " + result); // This line won't execute
        } catch (ArithmeticException e) {
            System.out.println("Caught ArithmeticException: " + e.getMessage());
        }
        System.out.println("\n--- Next Example ---\n");
        // Demonstrate ArrayIndexOutOfBoundsException
        try {
            int[] numbers = new int[35];
            System.out.println(numbers[36]); // This will cause an ArrayIndexOutOfBoundsException
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Caught ArrayIndexOutOfBoundsException: " + e.getMessage());
        }
        System.out.println("Program continues after handling exceptions.");
    }
}

Output:

Caught ArithmeticException: / by zero
--- Next Example ---
Caught ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 5
Program continues after handling exceptions.

The catch block runs in these examples if a matched exception is thrown, while the try block watches the code for mistakes.

Exception Propagation

If an exception is not handled at the location where it occurred, it propagates up the call stack. This process is known as exception propagation.

The default exception handler in the Java Virtual Machine will catch any exceptions that your program fails to capture. In addition to terminating program execution, this handler outputs a stack trace from the exception’s location and shows a text explaining the issue. The stack trace, which displays the order of method calls that resulted in the issue, is essential for debugging.

Programmers can use the throws keyword to explicitly propagate exceptions. A method’s throws clause in its method signature must declare any checked exceptions that it can generate but does not internally handle with a try-catch block. The responsibility is passed up the call stack to calling methods, who are then notified that they must either handle the declared exception or declare it themselves. It is not necessary to declare unchecked exceptions (subclasses of RuntimeException) in a throws clause since Java assumes that a function may throw them implicitly.

Consider this propagation example using throws:

import java.io.IOException;
public class ExceptionPropagationDemo {
    // Method that declares it throws IOException
    static void readFile() throws IOException {
        System.out.println("Inside readFile() method.");
        // Simulating an I/O error by directly throwing an IOException
        throw new IOException("Failed to read file data.");
    }
    // Method that calls readFile() and propagates the IOException
    static void processFile() throws IOException {
        System.out.println("Inside processFile() method.");
        readFile(); // This call must either be in a try-catch or declared to throw IOException
    }
    public static void main(String[] args) {
        System.out.println("Inside main() method.");
        try {
            processFile(); // This call must be in a try-catch because processFile() declares IOException
        } catch (IOException e) {
            System.out.println("Caught IOException in main(): " + e.getMessage());
        }
        System.out.println("Program finished.");
    }
}

Output:

Inside main() method.
Inside processFile() method.
Inside readFile() method.
Caught IOException in main(): Failed to read file data.
Program finished.

This illustrates how the exception is propagated up the call stack until it is handled: readFile() throws an IOException, which processFile() does not handle, so it declares throws IOException. Before main() calls processFile(), it must surround it in a try-catch block to handle the IOException.

Custom Exceptions

Although Java has built-in exceptions for many common errors, you may need to handle situations unique to your application, in which case you can create custom exception classes to manage errors as part of your program’s overall exception handling strategy.

Creating a custom exception is as easy as defining a subclass of Exception (for a checked exception) or RuntimeException (for an unchecked exception). Like any other class, your custom exception class can have fields and methods to convey specific error information, you should provide constructors that allow a detailed message to be passed, and you may want to override the toString() method to provide a customised description of the exception when it’s printed.

Here’s an example of a custom checked exception:

// Custom checked exception for an invalid age
class InvalidAgeException extends Exception {
    private int age;
    public InvalidAgeException(String message, int age) {
        super(message); // Call the superclass (Exception) constructor
        this.age = age;
    }
    public int getAge() {
        return age;
    }
    // Override toString to provide a custom message
    @Override
    public String toString() {
        return "InvalidAgeException: Age provided was " + age + ". " + super.getMessage();
    }
}
public class CustomExceptionDemo {
    public static void validateAge(int age) throws InvalidAgeException {
        if (age < 0 || age > 150) {
            throw new InvalidAgeException("Age must be between 0 and 150.", age);
        } else if (age < 18) {
            throw new InvalidAgeException("User must be at least 18 years old.", age);
        }
        System.out.println("Age " + age + " is valid.");
    }
    public static void main(String[] args) {
        System.out.println("Testing age validation:");
        try {
            validateAge(25);
            validateAge(15);
            validateAge(-5); // This will cause an exception
        } catch (InvalidAgeException e) {
            System.out.println("Caught custom exception: " + e.toString());
            System.out.println("Problematic age: " + e.getAge());
        } finally {
            System.out.println("Custom exception handling demonstration complete.");
        }
    }
}

Output:

Testing age validation:
Age 25 is valid.
Caught custom exception: InvalidAgeException: Age provided was 15. User must be at least 18 years old.
Problematic age: 15
Custom exception handling demonstration complete.

InvalidAgeException is a custom checked exception in this example, and the validateAge method declares that it throws InvalidAgeException. If an invalid age is passed, an InvalidAgeException object is created and thrown, which is then caught and processed in main(). The overridden toString() method gives a more detailed output about the particular age that caused the problem.

Index