Mastering Exception Handling in Java

The previous article provided an overview of the runtime aspects of error handling in programming. We learned about two different approaches based on status codes and exceptions. This article will dive deep into exception handling and its usage for writing robust and maintainable code.

Examples of Runtime Error

The following code snippet, discussed at the beginning of the previous article, demonstrated how an error in the program logic can lead to a runtime error condition:

int size = 5;
int index = 8 ; // an array element to access, provided as input

// declare an array of 'size' elements: 
// first index 0, last index 'size'-1
double[] values = new double[size]; 

values[0] = -1.1; // value stored in position 0 (first)
values[size - 1] = -8.7; // value stored in last position
values[index] = 4.557; // an error will be generated
values[2] = 3.4; // value stored in position 3 (will not be executed)

Whilst the above code does not generate any errors at compile time, its execution results in an exception of type ArrayIndexOutOfBoundsException raised due to the attempt to update an element of the values array beyond its allocated memory space (i.e., a non-existing element with index 8).

We know that if not properly handled, this exception determines the abnormal termination of the program, which is the default outcome when a program fails to address exceptions. Although we discussed that this exceptional situation could have been prevented by a simple check on the content of the index variable (being less than size), this example serves as a reminder that exceptions cannot be casually ignored.

Now, let’s consider another example where an exception could be raised, but it is not possible to prevent it by fixing the program logic, as shown in the following code snippet:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter an integer: ");
        int a = Integer.parseInt(scanner.nextLine());
        System.out.println(a + 1);

        // other instructions

        System.out.println("End of the Program");
    }
}

In this example, the program prompts the user to enter an integer via the console. The input is read as a String using the Scanner class and then converted to an int using the Integer.parseInt method. If the user enters a value like 56.7, which is a valid floating-point format but not suitable for an integer, the program will throw an exception and terminate printing:

Exception in thread "main" java.lang.NumberFormatException: For input string: "56.7".

The NumberFormatException is a common issue when converting strings to numbers. This exception does not indicate a flaw in the program’s logic; rather, it indicates that the input provided does not meet the expected format for conversion.

As a result of the exception thrown, the program will be interrupted, and all the instructions below the call to the parseInt method will not be executed. This includes the last instruction that prints the message “End of the Program”.

Handling Exceptions

When an exception is thrown, the runtime environment, e.g., the Java Virtual Machine (JVM) or the .NET CLR, searches for code capable of handling the exception.

The default exception-handling behaviour discussed above (i.e., abnormal program termination) only occurs when no suitable handlers are found throughout the call stack. In modern programming languages, such as Java and C#, the try and catch keywords are utilised to establish an exception handler.

The try-catch block is the primary way to handle exceptions. Code that may throw exceptions can be enclosed within a try block, and the exceptions will be handled in one or more catch blocks.

try with Single catch Block

To handle the error raised during the execution of the previous code snippet gracefully, a try-catch block should be used when calling Integer.parseInt (or similar conversion methods). The following code snippet shows how catching the generated exception can effectively handle the above error condition, preventing an abnormal program termination:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        try {
            System.out.print("Enter an integer: ");
            int a = Integer.parseInt(scanner.nextLine());
            System.out.println(a + 1);
        } catch (NumberFormatException ex) {
            System.out.println("Invalid input: " + ex.getMessage());
        }

        // other instructions

        System.out.println("End of the Program");
    }
}

The try block of code is where an exception may occur. If an exception does occur while the program execution is in this block, the language runtime will direct the control flow to the first appropriate catch block defined in the program.

A catch block specifies the type of exception it can handle within the parentheses that follow the catch keyword. The catch block is executed when an exception of the corresponding type is thrown.

In this example, when the NumberFormatException is generated by the parseInt method, the program execution is not interrupted. Instead, the program flow is directed to the matching catch (NumberFormatException ex) statement, and a message is printed indicating the occurrence of an error.

After the execution of the catch, the program flow will continue with the instructions following the catch, including the last one that prints the message “End of the Program”. In addition to the initial message “Enter an integer:”, the output of the program execution would therefore be like:

Invalid input: For input string: "56.7"

End of the Program

try with Multiple catch Blocks

Exception handling in Java, C#, and other programming languages can use multiple catch blocks within a try-catch construct to handle various exceptions effectively. Each catch block handles a specific category of exceptions, allowing for tailored responses based on the type of exception encountered.

Considering the Integer.parseInt example discussed above, it is known that the conversion operation can lead to a NumberFormatException if the input value is not a valid integer. Let’s consider an extended version of that program, which reads two integer values as input and performs a division operation. Now, this new program version could also raise an ArithmeticException if, for example, division by zero is attempted.

Whilst a simple check of the value of the denominator could be used to prevent this exception, for the sake of showing the usage of multiple catch blocks, let’s address this specific scenario by adding an additional catch block for ArithmeticException to the code:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        try {
            System.out.print("Enter an integer: ");
            int a = Integer.parseInt(scanner.nextLine());
            System.out.print("Enter another integer to divide by: ");
            int b = Integer.parseInt(scanner.nextLine());
            int result = a / b;
            System.out.println("Result: " + result);
        } catch (NumberFormatException ex) {
            System.out.println("Invalid input: " + ex.getMessage());
        } catch (ArithmeticException ex) {
            System.out.println("Arithmetic error: " + ex.getMessage());
        } catch (Exception ex) {
            System.out.println("An unexpected error occurred: " + ex.getMessage());
        }

        // other instructions

        System.out.println("End of the Program");
    }
}

When an exception arises within the try block, the runtime system evaluates each catch block in sequence. It checks whether the thrown exception type matches the type defined in a catch block. If a match is found, the corresponding catch block is executed, and the program continues from the end of the last catch block. If no matching catch block is identified, the exception is passed up the call stack to be handled by a higher-level error handler.

In the new code example, the first catch block is dedicated to handling NumberFormatException. If a NumberFormatException is thrown during the Integer.parseInt operation, this catch block takes action, delivering a specific error message. Following that, the second catch block addresses ArithmeticException. When a division by zero occurs, this block is executed and provides a custom error message.

Using multiple catch blocks, the program can handle different exceptions separately, offering more specific error messages and ensuring a more composed response to various issues. Depending on the exception raised and caught, the output of the program execution would, therefore, include:

Invalid input: For input string: "56.7"

End of the Program

or

Arithmetic error: / by zero

End of the Program

The above code also shows the good practice of including a catch block for generic exceptions (often just referred to as Exception). The generic Exception catch block acts as a safety net, providing a fallback mechanism for any exceptions not explicitly caught by the preceding catch blocks. This helps maintain the program's robustness by preventing it from crashing due to unanticipated errors.

Understanding Exception Objects

The previous code snippets demonstrate an important concept related to exceptions. You can see an identifier ex was added after the type of exception handled by each catch block.

Essentially, every exception is an object that a program can accessex is a reference type variable that references the generated exception object. As with every object, exceptions have methods and attributes. In Java, exception objects provide a getMessage method, which can be called when that exception is handled to retrieve a string describing the reason for the exception. Likewise, in C#, the same string is accessible via the corresponding Message property of the generated exception object.

Robust User Input Handling

The previous error handling was limited to showing a message on the screen to inform the user that the provided input format was invalid. However, a more advanced exception-handling strategy could be based on implementing a loop whereby the user is asked to re-input the data until a valid value is provided.

This is shown in the code snippet below:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        boolean isValidInput = false;
        Scanner scanner = new Scanner(System.in);

        while (!isValidInput) {
            System.out.print("Enter an integer: ");
            try {
                int a = Integer.parseInt(scanner.nextLine());
                System.out.print("Enter another integer to divide by: ");
                int b = Integer.parseInt(scanner.nextLine());
                int result = a / b;
                System.out.println("Result: " + result);
                isValidInput = true;
            } catch (NumberFormatException ex) {
                System.out.println("Invalid input: " + ex.getMessage() + " Please try again.");
            } catch (ArithmeticException ex) {
                System.out.println("Arithmetic error: " + ex.getMessage() + " Please try again.");
            } catch (Exception ex) {
                System.out.println("An unexpected error occurred: " + ex.getMessage() + " Please try again.");
            }
        }

        // other instructions

        System.out.println("End of the Program");
    }
}

When executed, the above code commences with a user prompt for entering an integer within a loop that runs while the content of the isValidInput variable is false. Like in the previous code example, after receiving the user’s input, the code enters a try block, where it attempts to convert the input to an integer using Integer.parseInt.

If the input is a non-integer, a NumberFormatException exception is triggered, directing the program to the corresponding catch block without executing the next instructions within the try block. Inside the catch block, the program displays an error message derived from the exception calling getMessage.

Similarly, if the user attempts to divide by zero, an ArithmeticException is triggered, and the program displays an appropriate error message using the ex object. The generic Exception catch block captures any other unexpected errors, ensuring the program can gracefully handle unforeseen issues.

Subsequently, unlike the previous code example, the program gracefully loops back to the beginning of the user input prompt, allowing repeated attempts. The program execution continues this way until valid integers are provided and the content of the isValidInput variable is set to true. At this point, the program displays the successfully computed result and exits the loop. All the remaining program instructions after the catchblocks will then be executed, including the last one that prints the message “End of the Program.”

Using the finally Block

In addition to try and catch blocks, programming languages provide the finally block to ensure that certain code is executed regardless of whether an exception occurred. The finally block is typically used to guarantee that required cleanup operations are performed even if an exception is thrown. The following code snippet extends the previous one by introducing a finally block at the end of the try-catch construct:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        boolean isValidInput = false;
        Scanner scanner = new Scanner(System.in);

        while (!isValidInput) {
            System.out.print("Enter an integer: ");
            try {
                int a = Integer.parseInt(scanner.nextLine());
                System.out.print("Enter another integer to divide by: ");
                int b = Integer.parseInt(scanner.nextLine());
                int result = a / b;
                System.out.println("Result: " + result);
                isValidInput = true;
            } catch (NumberFormatException ex) {
                System.out.println("Invalid input: " + ex.getMessage() + " Please try again.");
            } catch (ArithmeticException ex) {
                System.out.println("Arithmetic error: " + ex.getMessage() + " Please try again.");
            } catch (Exception ex) {
                System.out.println("An unexpected error occurred: " + ex.getMessage() + " Please try again.");
            } finally {
                System.out.println("This code always gets executed, regardless of exceptions.");
            }
        }

        // other instructions

        System.out.println("End of the Program");
    }
}

As said, the finally block usually contains code guaranteed to be executed, whether an exception is thrown or not. In this case, a message is just displayed to illustrate that the block is executed unconditionally.

In reality, the finally block primarily ensures that essential cleanup operations, such as closing files or releasing resources, are performed to maintain the program’s integrity. This block helps maintain an application's predictable and stable state, even when exceptions occur during its execution.

Throwing Exceptions

In Java, the throw keyword is used to explicitly throw an exception. This is useful when you want to signal an error condition from a method. As discussed, when an exception is thrown, the normal flow of the program is disrupted, and the runtime system searches for an appropriate exception handler to catch and handle the exception.

In the following example, the BankAccount class uses exceptions to handle error conditions such as negative balances and insufficient funds. This approach is an alternative to using status codes, which was discussed in a previous post:

public class BankAccount {
    private String number;
    private double balance;

    public BankAccount(String num, double bal) {
        if (bal < 0)
            throw new IllegalArgumentException("The initial balance on " + num + " cannot be negative");

        number = num;
        balance = bal;
    }

    public void deposit(double amount) {
        if (amount < 0)
            throw new IllegalArgumentException("The amount to deposit on " + number + " cannot be negative");

        balance += amount;
    }

    public void withdraw(double amount) {  
        if (amount < 0)
            throw new IllegalArgumentException("The amount to withdraw from " + number + " cannot be negative");

        else if (amount > balance)
            throw new IllegalStateException("Not enough money on account " + number + ", balance: " + balance);

        balance -= amount;
    }

    public double getBalance() {
        return balance;
    }

    public String getNumber() {
        return number;
    }
}

An exception object is created using the throw and new keywords, and contains information about the error, such as the error message and the exception type:

throw new IllegalArgumentException("The initial balance on " + num + " cannot be negative");

In the above example, IllegalArgumentException is created to indicate the attempt to assign a negative value to the balance attribute received as a parameter of the methods deposit, withdraw or the constructor. The above snippet shows how the generated exception object has the associated message attribute (accessible via the method getMessage) set to the string passed in parenthesis.

A generated exception disrupts the normal program execution flow and the associated exception object needs to be caught (and used) in an appropriate exception handler. Therefore, the runtime system (e.g., the JVM) searches for the nearest enclosing try-catch block that can handle the exception. This search starts from the method where the exception was thrown and proceeds up the call stack, i.e., going back to the method that invoked the one generating the exception.

The following code shows that the main method contains the instruction account.deposit(money), which attempts to deposit on account the amount stored in the money variable read as user input:

public static void main(String[] args) {
    BankAccount account = new BankAccount("AB123", 100);
    try {
        int money = // -100.56 read as input
        account.deposit(money);
    } catch (IllegalArgumentException e) {
        System.out.println("Error while performing operation: " + e.getMessage());
    }

    // other instructions

    System.out.println("End of the Program");
}

The instruction is safely enclosed within a try because if the content of money is negative (e.g., -100.56) the call to the deposit method will generate IllegalArgumentException. This type of exception is properly handled via a matching catch block that prints an error message and allows the program to continue executing the remaining instructions.

If the erroneous call to the deposit method was not done within a proper try-catch statement, the program would have terminated abnormally without reaching the end (i.e., the System.out.println("End of the Program")) and the uncaught exception would have been shown to the console:

Exception in thread "main" java.lang.IllegalArgumentException: The amount to deposit on AB123 cannot be negative

Conclusions

In this blog post, we explored the fundamentals of exception handling in Java, including handling runtime errors using single and multiple catch blocks, understanding exception objects, and implementing robust user input handling. We also discussed using the finally block to ensure certain code is executed regardless of exceptions and demonstrated how to throw exceptions using the throw keyword with a practical example.

While we have covered many essential aspects of exception handling, advanced topics still need to be discussed. Future posts will delve into the differences between checked and unchecked exceptions and how to create custom exceptions. These topics require a deeper understanding of inheritance and object-oriented programming, which will be explored in upcoming posts.