Photo by Sarah Kilian on Unsplash
Overview of Error Handling in Java and C#
Using status codes versus Exceptions
Encountering errors is a natural part of the software development process. They can stem from various sources, including minor oversights, logic issues, or unexpected user inputs. How we address these errors can significantly impact the performance and reliability of our software.
This article gives an overview of the fundamental concepts of errors and exceptions. We will examine the different types of errors and the tools programming languages provide to handle them. The art of error handling safeguards our code, ensuring it responds seamlessly to unforeseen issues and maintains its operational integrity.
What are Exceptions?
In programming, the term exception refers to an anomalous or exceptional condition that can occur during the execution of a program. Typically, an exception disrupts the regular flow of program execution and triggers a predefined exception handler.
This concept is grounded in the idea that every procedure—or method in object-oriented programming—has a specific set of conditions, known as preconditions, under which it should terminate "normally." When these preconditions are violated, an exception-handling mechanism enables the procedure to raise an exception, signalling that the expected conditions have not been met.
In a previous article about arrays, we briefly discussed the occurrence of an exception when a program attempted to access a memory location beyond the boundaries of an array. For example, consider the following Java code snippet:
int size = 5;
int index = ... ; // 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)
And assume the size
variable that defines the number of elements of the values
array contains 5
. The index
variable is assigned the value 8
, which was provided as input to the program.
For an array of size
elements, the index size - 1
always identifies the last element. Therefore, the instruction values[index] = 4.557
attempts to assign the literal 4.557
to an element of the values
array that does not exist. Specifically, the last element of the values
array in this example is associated with the index 4
, and the code is attempting to access an element with the index 8
.
Since there is no code to handle this exception in the example—the exception handler—the program execution will terminate abnormally, and the following message will appear on the Console:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 8 out of bounds for length 5 at exceptions.Exceptions.main
A similar message would printed by the C# runtime on the Console:
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
The particular type of exception generated in this example suggests that something might be wrong with the logic implemented to access the array elements based on the values stored in the variables size
and index
. In other words, the exception could have been prevented by simply checking that index<size && index>=0
before attempting to assign the value 4.557
, as demonstrated in the next section.
Error Handling: Status Codes versus Exceptions
According to what we learned from running the previous program, we can update the code to include a simple check on the content of the index
variable:
int size = 5;
int index = ... ; // 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
// Check if the index is within the valid range
// before accessing the array
if (index >= 0 && index < size) {
values[index] = 4.557; // value stored if index is valid
} else {
System.out.println("Error: Index " + index + " is out of bounds for array of size " + size);
}
values[2] = 3.4; // value stored in position 2
The previous code snippet has been enhanced for increased robustness by verifying the validity of the index
content provided as input and used to access an array element. This updated code validates the array index being accessed, effectively preventing the occurrence of the ArrayIndexOutOfBoundsException. Including the conditional check if (index >= 0 && index < size)
now ensures that any problematic assignment will not be executed, preventing the previous exception from happening.
More complex object-oriented programs group the instructions for a given operation as methods. A method could perform a check similar to the previous example and generate a boolean
value that indicates the outcome of the operation. The value can be returned as a status code and checked by other methods after invocation. This approach to error handling is often used for non-complex programs where exception handling would be an overkill.
Status Codes
Having methods return a value to signal the success or failure of an operation is a commonly adopted approach for a program to identify different types of errors. However, this strategy places the responsibility on developers to be watchful if they want to catch all errors, as it requires checking the return value for every operation.
To understand this concept, let’s consider the following implementation of a BankAccount
class:
public class BankAccount {
private String number;
private double balance;
public BankAccount(String num, double bal) {
if (bal < 0 ) {
System.out.println("Error: The initial balance on " + number + " cannot be negative");
} else {
number = num;
balance = bal;
}
}
public boolean deposit(double amount) {
if (amount < 0) {
System.out.println("Error: The amount to deposit from " + number + " cannot be negative");
return false;
}
balance += amount;
return true;
}
public boolean withdraw(double amount) {
if (amount < 0) {
System.out.println("Error: The amount to withdraw from " + number + " cannot be negative");
return false;
} else if (amount > balance) {
System.out.println("Error: Not enough money on account " + number + ", balance: " + balance);
return false;
}
balance -= amount;
return true;
}
public double getBalance() {
return balance;
}
public String getNumber() {
return number;
}
}
Where the methods deposit
and withdraw
return a boolean
value as a status code indicating the outcome of the respective transactions. Now, an external class can create BankAccount
objects, perform deposit and withdrawal operations by invoking the corresponding methods, and check whether the operations succeeded via the returned status codes:
public class Program {
public static void main(String[] args) {
BankAccount acc1 = new BankAccount("AB123", 100.0);
BankAccount acc2 = new BankAccount("CD456", 0.0);
boolean depositResult = acc1.deposit(200.8);
if (depositResult == true) {
System.out.println("Money successfully added on account " + acc1.getNumber());
} else {
System.out.println("Deposit failed on account " + acc1.getNumber());
}
if (acc2.deposit(130.5) == true) {
System.out.println("Money successfully added on account " + acc2.getNumber());
} else {
System.out.println("Deposit failed on account " + acc2.getNumber());
}
double withdrawAmount = -200.35;
if (acc1.withdraw(withdrawAmount)) {
System.out.println("Successful withdrawal! Balance on " + acc1.getNumber() + ": " + acc1.getBalance());
} else {
System.out.println("Withdrawal failed on " + acc1.getNumber());
}
if (acc2.withdraw(1000.50)) {
System.out.println("Successful withdrawal! Balance on " + acc2.getNumber() + ": " + acc2.getBalance());
} else {
System.out.println("Withdrawal failed on " + acc2.getNumber());
}
}
}
While this approach is possible, it can make the code less transparent and more challenging to maintain because extensive error checking can overshadow the logical flow of what should happen when everything goes smoothly.
To tackle this concern, Java, C#, and many other high-level programming languages provide a prevalent error-handling mechanism centred on the concept of exception discussed earlier. Exceptions enable a degree of separation between error-handling logic and the code that tries to perform the task.
The next post on exceptions will present the same BankAccount
class example, where status codes have been replaced by exceptions that the main
can use to recover from runtime error conditions. An overview of this approach is presented in the following section.
Exceptions
Instead of disregarding the possibility of an exception, as seen in the first code example of this article, a programmer is encouraged to catch and handle exceptions, leveraging them to regain control over the program execution rather than allowing the program to terminate prematurely.
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:
public class Program {
public static void main(String[] args) {
BankAccount account = new BankAccount("AB123", 100);
try {
int money = // -100.56 read as input
account.deposit(money);
} catch (/* error that the deposit methof could generate */) {
/* handle the error */
}
// other instructions
System.out.println("End of the Program");
}
The usage of the try-catch
block will be extensively discussed in the next article. For now, it should be noted from the above code example how exception handling via try
and catch
enables a clear separation between a task logic and the associated error-handling code, thus preventing potential concerns about code clarity related to status code checking.
More importantly, exceptions also serve as a means to communicate issues with situations where using a return code would be impractical, e.g., operations that may not be possible or may fail due to the current state of the world.
To better understand this concept, let’s consider a Java program that attempts to read data from a file. In this example, the (code returned by the) method exists
method of the java.nio.file.Files
class may be used to confirm a given file's existence. However, even after verifying the file's existence, the same program may still encounter errors indicating unexpected issues that require handling in the code, e.g., that file may become corrupted, inaccessible, or may have been deleted in the meantime. Therefore, attempting to access the file may raise an exception, which must be handled accordingly to prevent the execution of a program from being abruptly terminated.
Conclusions
While status codes can be useful for signalling the success or failure of operations, they require developers to consistently check return values, which can clutter the code and obscure the main logic. This approach suits simple, predictable scenarios but can become cumbersome in more complex applications.
In the next article, we will see how exceptions, on the other hand, offer a more robust and maintainable solution by separating error-handling logic from the main code. They are particularly effective for handling unpredictable errors, such as file I/O issues, where the state of external resources can change unexpectedly. We will dive deep into the details of the exception-handling process, explaining how exceptions can be caught and how they can be used in our code to signal exceptional situations instead of error codes.