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 have a significant impact on the performance and reliability of our software.
This article delves into the fundamental concepts of errors and exceptions in C#. We will examine the different types of errors and the tools that C# provides to handle them. The art of exception handling serves as a safeguard for our code, ensuring that it responds seamlessly to unforeseen issues and maintains its operational integrity.
What are Exceptions?
In the context of 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.
For example, 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. The code snippet below shows an example of instructions leading to such an exceptional condition.
int size = // provided as input: 5
int index = // an array element to access, calculated: 8
// 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)
In the example, we assume the size
variable that defines the number of elements of the values
array is provided as user input and contains 5
. The index
variable is assigned the value 8
, calculated in the second instruction based on a given logic implemented in the code.
It is important to note that 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—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:
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
The particular type of exception in this example suggests that something might be wrong with the logic that calculates (and assigns to the index
variable) the array position to be accessed or with the array size provided as user input. As such, there might be a fundamental error with the logic of the application that the developer should fix instead of handling the exception using the try-catch
construct discussed later in the article.
Error Codes versus Exceptions
Another error-handling approach could be considered in the case when the value assigned to the index
variable is (for some reason) also provided as an external input to the program, as shown below.
int size = // provided as input: 5
// declare an array of 'size' elements: first index 0, last index 'size'-1
double[] values = new double[size];
int index = // an array element to access, provided as input: 8
bool result; // stores the result of the index boundary check
double value = 4.557;
// values.Length is equivalent to size
if (index < 0 || index >= values.Length)
{
result = false;
}
else
{
result = true;
values[index] = value;
}
if (result)
{
Console.WriteLine($"Value {value} added at index {index}");
}
else
{
Console.WriteLine("Error: Index is out of array boundaries");
}
// do more things
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 both the array size and the index being accessed, effectively preventing the occurrence of the IndexOutOfRangeException.
As in the previous example, we assume that size = 5
and index = 8
, but this time they have both been received as user input. Therefore, potential errors are now related to the validity of that input rather than the logic used to determine the array position to be accessed. Including the conditional check if (index < 0 || index >= values.Length)
now ensures that any problematic assignment will not be executed, preventing the previous exception from happening.
Taking this one step further, the instructions for validating the content of the index
and size
variables could be encapsulated in a method called WriteAt(double[] a, double v, int i)
. This method can be invoked whenever there is a need to add a value v
to an array a
at a specific index position i
. The method should also return a status code indicating the outcome of the operation, possibly a boolean, as shown in the program snippet. This status code can be checked by any other method calling WriteAt
.
Error 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. 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, C# and many other high-level programming languages provide a prevalent error-handling mechanism centred on the concept of exception discussed earlier. This enables a degree of separation between error-handling logic and the code that tries to perform the task.
Exceptions
Instead of disregarding the possibility of an exception, as seen in the first code example, 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.
Not only do exceptions prevent the above concerns related to error code checking, but they 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.
For instance, if a program attempts to read data from a file, the (code returned by the) method File.Exists
may be used to confirm the 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.
The remainder of the article will dive deep into the details of the exception-handling process.
Handling Exceptions
The first code snippet demonstrated the default outcome when a program fails to address exceptions: simply crashes. This serves as a reminder that exceptions cannot be casually ignored. When an exception is thrown, the Common Language Runtime (CLR) searches for code capable of handling the exception. The default exception-handling behaviour occurs only when no suitable handlers are found throughout the call stack. C#'s try
and catch
keywords are utilised to establish an exception handler.
try-catch
Blocks
In C#, 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. The code example below shows the basic structure of a try-catch
block.
try
{
// Code that might throw an exception
}
catch (ExceptionType ex)
{
// Handle the exception
}
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 CLR 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 parenthesis following the catch
keyword. When an exception of the specified type is thrown, the corresponding catch
block is executed. It is possible to have multiple catch
blocks for different exception types to provide specific error handling for each case.
Exception Objects
The above code snippet also demonstrates another important concept related to exceptions. An identifier ex
has been added after the type of exception (ExceptionType
) handled by the catch
.
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—or properties in the C# specific jargon. Like the Length
property of arrays, exceptions have a Message
property that contains a string describing the reason for the exception and can be accessed when that exception is handled.
Exception Handling Example: Convert.ToInt32
In previous articles in this series, we demonstrated the use of Console.ReadLine()
in combination with Convert.ToInt32()
(or Convert.ToDouble()
) to collect and store user input as int
(or double
) values. However, the examples assumed that the provided input was always valid, which is not necessarily the case.
To illustrate, consider the following code snippet:
Console.Write("Enter an integer: ");
string userInput = Console.ReadLine();
int number = Convert.ToInt32(userInput);
When a 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 then suddenly terminate:
Unhandled exception. System.FormatException: The input string '56.7' was not in the correct format.
This FormatException
exception does not indicate a flaw in the program's logic; it simply reflects the input provided. To handle such errors gracefully, a try-catch
block should be used when calling Convert.ToInt32()
(or similar conversion methods). The following code snippet shows how catching the generated exception can effectively handle the above error condition.
Console.Write("Enter an integer: ");
string userInput = Console.ReadLine();
try
{
int number = Convert.ToInt32(userInput);
Console.WriteLine("You entered: " + number);
}
catch (FormatException)
{
Console.WriteLine("Format Exception. Please try again.");
}
The above error handling is limited to showing a message on the screen to inform the user about the format of the provided input not being valid. 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. Moreover, inside the catch
block, ex
is now used to access and print on the screen the message explaining the reason for the exception based on the Message
property of the generated exception.
bool isValidInput = false;
while (!isValidInput)
{
Console.Write("Enter an integer: ");
string userInput = Console.ReadLine();
try
{
int number = Convert.ToInt32(userInput);
Console.WriteLine("You entered: " + number);
isValidInput = true;
}
catch (FormatException ex)
{
Console.WriteLine("Format Exception: " + ex.Message + " Please try again.");
}
}
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
. After receiving the user's input, the code enters a try
block where it attempts to convert the input to an integer using Convert.ToInt32
.
However, if the input is a non-integer, a FormatException
exception is triggered, directing the program to the catch
block without executing the next two instructions within the try
block. Inside the catch
block, the program displays an error message derived from the exception.
Subsequently, the program gracefully loops back to the beginning of the user input prompt, allowing repeated attempts. Execution continues this way until a valid integer is provided and the content of the isValidInput
variable is true
. At this point, the program displays the successfully converted value and exits the loop.
Multiple Catch Blocks
Exception handling in C# (and other programming languages) can use multiple catch
blocks within a try-catch
construct to handle various types of exceptions effectively. Each catch
block handles a specific category of exceptions, allowing for tailored responses based on the type of exception encountered.
When an exception arises within the try
block, the runtime system evaluates each catch
block in sequence. It checks whether the type of the thrown exception 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.
Considering the Convert.ToInt32
example discussed above, it is known that the conversion operation can lead to an overflow exception if the input value is too large (or too small). To address this specific scenario, an additional catch
block for OverflowException
can be added to the previous code:
bool isValidInput = false;
while (!isValidInput)
{
Console.Write("Enter an integer: ");
string userInput = Console.ReadLine();
try
{
int number = Convert.ToInt32(userInput);
Console.WriteLine("You entered: " + number);
isValidInput = true;
}
catch (FormatException ex)
{
Console.WriteLine("Format Exception: " + ex.Message + " Please try again.");
}
catch (OverflowException ex)
{
Console.WriteLine("Overflow Exception: " + ex.Message + " Please try again.");
}
catch (Exception)
{
Console.WriteLine("An unexpected error occurred. Please try again.");
}
}
The above code also shows the good practice of including a catch
block for generic exceptions (often just referred to as Exception
). This catch
block can capture any unanticipated issues that may arise and provide a fallback error-handling mechanism.
In the new code example, the first catch
block is dedicated to handling FormatException
. If a FormatException
is thrown during the Convert.ToInt32
operation, this catch
block takes action, delivering a specific error message.
Following that, the second catch
block addresses OverflowException
. When the input value is too large (or too low) and triggers an overflow, this block provides a custom error message.
Lastly, the catch
block for Exception
acts as a catch-all for unexpected exceptions not explicitly handled by the preceding catch blocks. It offers a generic error message and facilitates a more composed response to unforeseen issues.
Finally Block
In addition to try
and catch
blocks, C# provides 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.
bool isValidInput = false;
while (!isValidInput)
{
Console.Write("Enter an integer: ");
string userInput = Console.ReadLine();
try
{
int number = Convert.ToInt32(userInput);
Console.WriteLine("You entered: " + number);
isValidInput = true;
}
catch (FormatException ex)
{
Console.WriteLine("Format Exception: " + ex.Message + " Please try again.");
}
catch (OverflowException ex)
{
Console.WriteLine("Overflow Exception: " + ex.Message + " Please try again.");
}
catch (Exception)
{
Console.WriteLine("An unexpected error occurred. Please try again.");
}
finally
{
Console.WriteLine("This code always gets executed, regardless of exceptions.");
}
}
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 a predictable and stable state of an application, even when exceptions occur during its execution.