.NET exception handling is a unified approach to error handling. A developer is no longer required to write complex code in order to account for every error scenario. Instead, the code in question is wrapped with a try block, followed by a list of exceptions that the code can cause. The exception caught is a class that is filled with valuable information about the error.
Contributed by Ayad Boudiab Rating: / 6 June 17, 2008
Before we start with what exceptions are and how to use them in the .NET Framework, let's take a quick look at how we used to handle errors in other programming languages (like C):
int main()
{
int result = MyFunction();
//assume ERROR_FILE_NOT_FOUND is already defined
if(result == ERROR_FILE_NOT_FOUND)
printf("Error: file not found");
}
int MyFunction()
{
...
//the file was not found and we need to return an error
return ERROR_FILE_NOT_FOUND
}
Looking at this code, we can find at least two problems:
The constant ERROR_FILE_NOT_FOUND is merely a number. It does not give us enough information about the error and how to deal with it.
In case we need to handle more errors, we have to build more and more embedded ifs to account for all scenarios. This leads to complicated and unstructured code.
To solve this issue, the .NET Framework provides a more structured approach to error handling. This approach is the same for all programming languages under the .NET Framework (C#, VB.NET...). Knowing that .NET languages are object-oriented, it should not come as a surprise that exceptions are classes. Every Exception class encapsulates all the necessary information about an exception (even its inner exception that we will explore shortly). This information includes:
A help link to a help file associated with the exception
A descriptive message
The source (the name of the application or the object that causes the error)
A stack trace of the method calls that lead to the exception
These bits of information are bundled together to give us a more meaningful interpretation of what exactly happens and where. Armed with these exceptions, developers can easily catch many errors and handle them gracefully.
The .NET Framework provides a list of exceptions that you can use in your applications. Examples of these exceptions include IndexOutOfRangeException, IOException, FileNotFoundException, etc. All exceptions are in an inheritance hierarchy leading to the top where the Exception class lives (Exception is the parent of all exceptions). Exceptions come in two categories: ApplicationException and SystemException.
SystemException: This exception is thrown by the common language runtime when recoverable errors occur (such as an array out-of-bounds error).
ApplicationException: This exception is thrown by a user program, not the runtime. Inherit from this class when designing your own exception hierarchy.
C# provides four keywords that deal with exceptions: try, catch, throw, and finally. Before we discuss those keywords, let's start with an example that intentionally throws an exception and work our way through to handling the exception. One good example will be accessing an element outside the bounds of an array:
The array, a , contains 3 elements at positions 0, 1, and 2. Since there is no fourth element, the program will trigger an error when it tries to execute the line a[3] = 40;
In exception terminology, we say that an exception was thrown at the line a[3] = 40;
Because our program did not contain any error handling code, the exception (in this case IndexOutOfRangeException) is bubbled up all the way to the runtime, which handles the exception, prints it on the screen, and terminates the program.
We can improve this program by handling the exception ourselves. To do this, we surround the code that could cause an exception with a try block, and catch the exception after the try block as such:
class Program
{
static void Main( string [] args)
{
int [] a = new int [3];
try
{
a[0] = 10; //ok
a[1] = 20; //ok
a[2] = 30; //ok
a[3] = 40; //No such element.
}
catch ( Exception e)
{
Console .WriteLine( "Error: accessing an element " +
"outside the bounds of the array!" );
}
}
}
So, after we surrounded the code with the try block, we added a catch that catches objects of type Exception. Since the Exception class is the parent of all exceptions, the catch block will catch any exception that might happen in the try block (not necessarily array-related exceptions). The code execution stops at the offending statement (a[3] = 40;), and falls down to the catch blocks (in this case, there is only one).
If the catch object matched the one being thrown, then the catch block is executed and all other catch blocks are skipped (nothing to skip in this case, because there is only one catch block). The execution is then transferred to any code after the catch blocks. Please note that the exception being thrown is ArrayIndexOutOfBoundException and yet it is caught by an Exception type. This happens because ArrayIndexOutOfBoundException is of type Exception (is-a relationship).
As mentioned earlier, an Exception object (or any of its descendants) holds very helpful details about the cause of the error. Let's modify the previous code to print out more details about the exception:
We factored out the code that prints exception information into a separate method called WriteExceptionDetails(). This method takes an Exception object as a parameter and prints out its details. This is helpful, since we can call this method anywhere in the code for any Exception object. In this case, we simply wrote the details to the output screen. Instead, we could have logged the details into a file that we can analyze later to better improve our code. The Exception object contains properties, such as Message (a nicely formatted message about the cause of the error), Source (the name of the program where the exception happened), and StackTrace (the sequence of calls that triggered the exception).
One of the important properties of the Exception class is InnerException. As the term implies, it is an exception within an exception. Here is an example that illustrates its usage:
The Exception constructor is overloaded to accept a message and another exception (the inner exception). We take advantage of this constructor by recording the exception that happened in PopulateArray(). Then, we can throw e2 (with its inner exception) to be caught by the calling method ( Main() ). After Main() catches the exception in its catch block, it writes the exception details to the screen (including the inner exception from PopulateArray()) by calling WriteExceptionDetails().
Since there are many types of exceptions that can be thrown, it comes as no surprise that we can have many catch blocks. Every catch block handles the type of exception that we expect to be thrown. Ponder the following code:
class Program
{
static void Main( string [] args)
{
int [] a = new int [3];
try
{
a[0] = 10; //ok
a[1] = 20; //ok
a[2] = 30; //ok
a[3] = 40; //No such element.
}
catch ( IndexOutOfRangeException e)
{
Console .WriteLine( "An index out of range exception has
occurred!" );
}
catch ( ArgumentException e)
{
Console .WriteLine( "An argument exception has occurred!" );
}
catch ( ArrayTypeMismatchException e)
{
Console .WriteLine( "An array type mismatch exception has
IndexOutOfRangeException is the best match for the type of exception that occurred in this code. Therefore, the output of the program will be: "An index out of range exception has occurred." The order of the catch blocks matters; we catch exceptions from the most specific to the most general. As a matter of fact, the compiler will issue an error message if you try to catch a more general exception before a specific exception (try to move catch (Exception e) and place it before catch (IndexOutOfRangeException e) and see what happens).
And finally, the finally block. As we discussed earlier, once a catch block is executed, the other catch blocks are skipped. But what happens if we need to run a piece of code (such as closing a stream) after we catch the exception? The first answer will be to duplicate the piece of code in every catch block to make sure it executes for any type of exception. To avoid this duplication of code issue, we can use the finally block. This code will run whether there was an exception or not. We use this block to close opened streams and do the necessary clean up before we leave the method. Execute the following code first as is, then by commenting out the line a[3] = 40; //No such element:
class Program
{
static void Main( string [] args)
{
int [] a = new int [3];
try
{
a[0] = 10; //ok
a[1] = 20; //ok
a[2] = 30; //ok
a[3] = 40; //No such element.
}
catch ( IndexOutOfRangeException e)
{
Console .WriteLine( "An index out of range exception has
occurred!" );
}
catch ( ArgumentException e)
{
Console .WriteLine( "An argument exception has occurred!" );
}
catch ( ArrayTypeMismatchException e)
{
Console .WriteLine( "An array type mismatch exception has
occurred!" );
}
catch ( Exception e)
{
//printing the exception details
WriteExceptionDetails(e);
}
finally
{
Console .WriteLine( "This line executes no matter what " +
"exception was thrown. As a matter of fact this code " +
"will run even if there were no exceptions" );
//Do all the necessary clean-up and close streams here
Exceptions are a powerful and clean mechanism used to track and handle errors that happen in a program. Every exception is equipped with properties and methods that return valuable information about the error. To handle exceptions, the code in question should be wrapped with a try block and followed by a list of catch blocks of exceptions that could happen. Make it a habit to always keep a finally block after the last catch block in order to do the necessary cleanup and close opened streams.