An exception is a runtime error in a program that violates a system or application constraint, or a condition that is not expected to occur during normal operation. Examples are when a program tries to divide a number by zero or tries to write to a read-only file. When these occur, the system catches the error and raises an exception.
If the program has not provided code to handle the exception, the system will halt the program. For example, the following code raises an exception when it attempts to divide by zero:
static void Main()
{
int x = 10, y = 0;
x /= y; // Attempt to divide by zero--raises an exception
}
When this code is run, the system displays the following error message:
Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
at Exceptions_1.Program.Main() in C:ProgsExceptionsProgram.cs:line 12
The try
statement allows you to designate blocks of code to be guarded for exceptions and to supply code to handle them if they occur. The try
statement consists of three sections, as shown in Figure 22-1.
- The
try
block contains the code that is being guarded for exceptions.- The
catch
clauses section contains one or morecatch
clauses. These are blocks of code to handle the exceptions. They are also known as exception handlers.- The
finally
block contains code to be executed under all circumstances, whether or not an exception is raised.
The previous example showed that attempting to divide by zero causes an exception. You can modify the program to handle that exception by placing the code inside a try
block and supplying a simple catch
clause. When the exception is raised, it is caught and handled in the catch
block.
static void Main()
{
int x = 10;
try
{
int y = 0;
x /= y; // Raises an exception
}
catch
{
... // Code to handle the exception
Console.WriteLine("Handling all exceptions - Keep on Running");
}
}
This code produces the following message. Notice that, other than the output message, there is no indication that an exception has occurred.
Handling all exceptions - Keep on Running
There are many different types of exceptions that can occur in a program. The BCL defines a number of exception classes, each representing a specific type. When one occurs, the CLR does the following:
- It creates an exception object for the type.
- It looks for an appropriate
catch
clause to handle it.
All exception classes are ultimately derived from the System.Exception
class. Figure 22-2 shows a portion of the exception inheritance hierarchy.
An exception object contains read-only properties with information about the exception that caused it. Table 22-1 shows some of these properties.
The catch
clause handles exceptions. There are three forms, allowing different levels of processing. Figure 22-3 shows the forms.
The general catch
clause can accept any exception but can’t determine the type of exception that caused it. This allows only general processing and cleanup for whatever exception might occur.
The specific catch
clause form takes the name of an exception class as a parameter. It matches exceptions of the specified class or exception classes derived from it.
The specific catch
clause with object form gives you the most information about the exception. It matches exceptions of the specified class, or exception classes derived from it. It gives you a reference to the exception object created by the CLR by assigning it to the exception variable. You can access the exception variable’s properties within the block of the catch
clause to get specific information about the exception raised.
For example, the following code handles exceptions of type IndexOutOfRangeException
. When one occurs, a reference to the actual exception object is passed into the code with parameter name e
. The three WriteLine
statements each read a string field from the exception object.
Exception type Exception variable
↓ ↓
catch ( IndexOutOfRangeException e )
{ Accessing the exception variables
↓
Console.WriteLine( "Message: {0}", e.Message );
Console.WriteLine( "Source: {0}", e.Source );
Console.WriteLine( "Stack: {0}", e.StackTrace );
Going back to our divide-by-zero example, the following code modifies the previous catch
clause to specifically handle exceptions of the DivideByZeroException
class. While in the previous example, the catch
clause would handle any exception raised in the try
block, the current example will only handle those of the DivideByZeroException
class.
int x = 10;
try
{
int y = 0;
x /= y; // Raises an exception
} Exception type
↓
catch ( DivideByZeroException )
{
...
Console.WriteLine("Handling an exception.");
}
You could further modify the catch
clause to use an exception variable. This allows you to access the exception object inside the catch
block.
int x = 10;
try
{
int y = 0;
x /= y; // Raises an exception
} Exception type Exception variable
↓ ↓
catch ( DivideByZeroException e )
{ Accessing the exception variables
↓
Console.WriteLine("Message: {0}", e.Message );
Console.WriteLine("Source: {0}", e.Source );
Console.WriteLine("Stack: {0}", e.StackTrace );
}
On my computer, this code produces the following output. On your machine, the file path in the third and fourth lines will be different, and will match the location of your project and solution directories.
Message: Attempted to divide by zero.
Source: Exceptions 1
Stack: at Exceptions_1.Program.Main() in C:ProgsExceptions 1
Exceptions 1Program.cs:line 14
The purpose of a catch
clause is to allow you to handle an exception in an elegant way. If your catch
clause is of the form that takes a parameter, then the system has set the exception variable to a reference to the exception object, which you can inspect to determine the cause of the exception. If the exception was the result of a previous exception, you can get a reference to that previous exception’s object from the variable’s InnerException
property.
The catch
clauses section can contain multiple catch
clauses. Figure 22-4 shows a summary of the catch
clauses section.
When an exception is raised, the system searches the list of catch
clauses in order, and the first catch
clause that matches the type of the exception object is executed. Because of this, there are two important rules in ordering the catch
clauses. They are the following:
- The specific
catch
clauses must be ordered with the most specific exception types first, progressing to the most general. For example, if you declare an exception class derived fromNullReferenceException
, thecatch
clause for your derived exception type should be listed before thecatch
clause forNullReferenceException
.- If there is a general
catch
clause, it must be last, after all specificcatch
clauses. Using the generalcatch
clause is discouraged because it can hide bugs by allowing the program to continue execution when your code should be handling the error in a specific way. It can also leave the program in an unknown state. Therefore, you should use one of the specificcatch
clauses if at all possible.
If a program’s flow of control enters a try
statement that has a finally
block, the finally
block is always executed. Figure 22-5 shows the flow of control.
- If no exception occurs inside the
try
block, then at the end of thetry
block, control skips over anycatch
clauses and goes to thefinally
block.- If an exception occurs inside the
try
block, then the appropriatecatch
clause in thecatch
clauses section is executed, followed by execution of thefinally
block.
The finally
block will always be executed before returning to the calling code, even if a try
block has a return
statement or an exception is thrown in the catch
block. For example, in the following code, there is a return
statement in the middle of the try
block that is executed under certain conditions. This does not allow it to bypass the finally
statement.
try
{
if (inVal < 10) {
Console.Write("First Branch - ");
return;
}
else
Console.Write("Second Branch - ");
}
finally
{ Console.WriteLine("In finally statement"); }
This code produces the following output when variable inVal
has the value 5
:
First Branch - In finally statement
When a program raises an exception, the system checks to see whether the program has provided a handler for it. Figure 22-6 shows the flow of control.
- If the exception occurred inside a
try
block, the system will check to see whether any of thecatch
clauses can handle the exception.- If an appropriate
catch
clause is found, then the following happens:
- The
catch
clause is executed.- If there is a
finally
block, it is executed.- Execution continues after the end of the
try
statement (that is, after thefinally
block, or after the lastcatch
clause if there is nofinally
block).
If the exception was raised in a section of code that was not guarded by a try
statement, or if the try
statement does not have a matching exception handler, the system will have to look further for a matching handler. It will do this by searching down the call stack, in sequence, to see whether there is an enclosing try
block with a matching handler.
Figure 22-7 illustrates the search process. On the left of the figure is the calling structure of the code, and on the right is the call stack. The figure shows that Method2
is called from inside the try
block of Method1
. If an exception occurs inside the try
block in Method2
, the system does the following:
- First, it checks to see whether
Method2
has exception handlers that can handle the exception.
- If so,
Method2
handles it, and program execution continues.- If not, the system continues down the call stack to
Method1
, searching for an appropriate handler.- If
Method1
has an appropriatecatch
clause, the system does the following:
- It goes back to the top of the call stack—which is
Method2
.- It executes
Method2
’sfinally
block and popsMethod2
off the stack.- It executes
Method1
’scatch
clause and itsfinally
block.- If
Method1
doesn’t have an appropriatecatch
clause, the system continues searching down the call stack.
Figure 22-8 shows the general algorithm for handling an exception.
In the following code, Main
starts execution and calls method A
, which calls method B
. A description and diagram of the process are given after the code and in Figure 22-9.
class Program
{
static void Main()
{
MyClass MCls = new MyClass();
try
{ MCls.A(); }
catch (DivideByZeroException e)
{ Console.WriteLine("catch clause in Main()"); }
finally
{ Console.WriteLine("finally clause in Main()"); }
Console.WriteLine("After try statement in Main.");
Console.WriteLine(" -- Keep running.");
}
}
class MyClass
{
public void A()
{
try
{ B(); }
catch (System.NullReferenceException)
{ Console.WriteLine("catch clause in A()"); }
finally
{ Console.WriteLine("finally clause in A()"); }
}
void B()
{
int x = 10, y = 0;
try
{ x /= y; }
catch (System.IndexOutOfRangeException)
{ Console.WriteLine("catch clause in B()"); }
finally
{ Console.WriteLine("finally clause in B()"); }
}
}
This code produces the following output:
finally clause in B()
finally clause in A()
catch clause in Main()
finally clause in Main()
After try statement in Main.
-- Keep running.
Main
callsA
, which callsB
, which encounters aDivideByZeroException
exception.- The system checks
B
’scatch
section for a matchingcatch
clause. Although it has one forIndexOutOfRangeException
, it doesn’t have one forDivideByZeroException
.- The system then moves down the call stack and checks
A
’scatch
section, where it finds thatA
also doesn’t have a matchingcatch
clause.- The system continues down the call stack and checks
Main
’scatch
clause section, where it finds thatMain
does have aDivideByZeroException
catch
clause.- Although the matching
catch
clause has now been located, it is not executed yet. Instead, the system goes back to the top of the stack, executesB
’sfinally
clause, and popsB
from the call stack.- The system then moves to
A
, executes itsfinally
clause, and popsA
from the call stack.- Finally,
Main
’s matchingcatch
clause is executed, followed by itsfinally
clause. Execution then continues after the end ofMain
’stry
statement.
You can make your code explicitly raise an exception by using the throw
statement. The syntax for the throw
statement is the following:
throw ExceptionObject;
For example, the following code defines a method called PrintArg
, which takes a string
argument and prints it out. Inside the try
block, it first checks to make sure the argument is not null
. If it is, it creates an ArgumentNullException
instance and throws it. The exception instance is caught in the catch
statement, and the error message is printed. Main
calls the method twice: once with a null
argument and then with a valid argument.
class MyClass
{
public static void PrintArg(string arg)
{
try
{
if (arg == null) Supply name of null argument.
{ ↓
ArgumentNullException myEx = new ArgumentNullException("arg");
throw myEx;
}
Console.WriteLine(arg);
}
catch (ArgumentNullException e)
{
Console.WriteLine("Message: {0}", e.Message);
}
}
}
class Program
{
static void Main()
{
string s = null;
MyClass.PrintArg(s);
MyClass.PrintArg("Hi there!");
}
}
This code produces the following output:
Message: Value cannot be null.
Parameter name: arg
Hi there!
The throw
statement can also be used without an exception object, inside a catch
block.
- This form rethrows the current exception, and the system continues its search for additional handlers for it.
- This form can be used only inside a
catch
statement.
For example, the following code rethrows the exception from inside the first catch
clause:
class MyClass
{
public static void PrintArg(string arg)
{
try
{
try
{
if (arg == null) Supply name of null argument.
{ ↓
ArgumentNullException myEx = new ArgumentNullException("arg");
throw myEx;
}
Console.WriteLine(arg);
}
catch (ArgumentNullException e)
{
Console.WriteLine("Inner Catch: {0}", e.Message);
throw;
} ↑
} Rethrow the exception, with no additional parameters.
catch
{
Console.WriteLine("Outer Catch: Handling an Exception.");
}
}
}
class Program {
static void Main() {
string s = null;
MyClass.PrintArg(s);
}
}
This code produces the following output:
Inner Catch: Value cannot be null.
Parameter name: arg
Outer Catch: Handling an Exception.
18.119.235.79