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 access—ex
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 catch
blocks 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.