Exception Handling in C#
Level Author
Intermediate Anonymous
What 抯 Wrong with Return Codes?
Most programmers have probably written code that looked like this:
bool success =CallFunction();
if (!success)
{
//process the error
}
This works okay, but every return value has to be checked for an error. If the above
was written as
CallFunction();
any error return would be thrown away. That抯 where bugs come from.
There are many different models for communicating status; some functions
may return an HRESULT , some may return a Boolean value, and others may use
some other mechanism.
In the .NET Runtime world, exceptions are the fundamental method of han-dling
error conditions. Exceptions are nicer than return codes because they can抰
be silently ignored.
Trying and Catching
To deal with exceptions, code needs to be organized a bit differently. The sections
of code that might throw exceptions are placed in a try block, and the code to handle
exceptions in the try block is placed in a catch block. Here抯 an example:
using System;
class Test
{
static int Zero =0;
public static void Main()
{
//watch for exceptions here
try
{
int j =22 /Zero;
}
//exceptions that occur in try are transferred here
catch (Exception e)
{
Console.WriteLine("Exception "+e.Message);
}
Console.WriteLine("After catch");
}
}
The try block encloses an expression that will generate an exception. In this case,
it will generate an exception known as DivideByZeroExceptio . When the division
takes place, the .NET Runtime stops executing code and searches for a try block
surrounding the code in which the exception took place. When it finds a try block,
it then looks for associated catch blocks.
If it finds catch blocks, it picks the best one (more on how it determines which
one is best in a minute), and executes the code within the catch block. The code in
the catch block may process the event or rethrow it.
The example code catches the exception and writes out the message that is
contained within the exception object.
The Exception Hierarchy
All C# exceptions derive from the class named Exception , which is part of the Common
Language Runtime 1 . When an exception occurs, the proper catch block is
determined by matching the type of the exception to the name of the exception
mentioned. A catch block with an exact match wins out over a more general
exception. Returning to the example:
using System;
class Test
{
static int Zero =0;
public static void Main()
{
try
{
int j =22 /Zero;
}
//catch a specific exception
catch (DivideByZeroException e)
{
Console.WriteLine("DivideByZero {0}",e);
}
//catch any remaining exceptions
catch (Exception e)
{
Console.WriteLine("Exception {0}",e);
}
}
}
The catch block that catches the DivideByZeroException is the more specific match,
and is therefore the one that is executed.
This example is a bit more complex:
using System;
class Test
{
static int Zero =0;
static void AFunction()
{
int j = 22 / Zero;
//the following line is never executed.
Console.WriteLine("In AFunction()");
}
public static void Main()
{
try
{
AFunction();
}
catch (DivideByZeroException e)
{
Console.WriteLine("DivideByZero {0}",e);
}
}
}
What happens here?
When the division is executed, an exception is generated. The runtime starts
searching for a try block in AFunction(), but it doesn抰 find one, so it jumps out of
AFunction(), and checks for a try in Main(). It finds one, and then looks for a catch
that matches. The catch block then executes.
Sometimes, there won抰 be any catch clauses that match.
using System;
class Test
{
static int Zero =0;
static void AFunction()
{
try
{
int j =22 /Zero;
}
//this exception doesn't match
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine("OutOfRangeException:{0}",e);
}
Console.WriteLine("In AFunction()");
}
public static void Main()
{
try
{
AFunction();
}
//this exception doesn't match
catch (ArgumentException e)
{
Console.WriteLine("ArgumentException {0}",e);
}
}
}
Neither the catch block in AFunction()nor the catch block in Main()matches the
exception that抯 thrown. When this happens, the exception is caught by the "last
chance" exception handler. The action taken by this handler depends on how the
runtime is configured, but it will usually bring up a dialog box containing the excep-tion
information and halt the program.
Passing Exceptions on to the Caller
It抯 sometimes the case that there抯 not much that can be done when an exception
occurs; it really has to be handled by the calling function. There are three basic
ways to deal with this, which are named based on their result in the caller: Caller
Beware, Caller Confuse, and Caller Inform.
Caller Beware
The first way is to merely not catch the exception. This is sometimes the right design
decision, but it could leave the object in an incorrect state, causing problems when the
caller tries to use it later. It may also give insufficient information to the caller.
Caller Confuse
The second way is to catch the exception, do some cleanup, and then rethrow
the exception:
using System;
public class Summer
{
int sum =0;
int count =0;
float average;
public void DoAverage()
{
try
{
average =sum /count;
}
catch (DivideByZeroException e)
{
//do some cleanup here
throw e;
}
}
}
class Test
{
public static void Main()
{
Summer summer =new Summer();
try
{
summer.DoAverage();
}
catch (Exception e)
{
Console.WriteLine("Exception {0}",e);
}
}
}
This is usually the minimal bar for handling exceptions; an object should always
maintain a valid state after an exception.
This is called Caller Confuse because while the object is in a valid state after
the exception occurs, the caller often has little information to go on. In this case,
the exception information says that a DivideByZeroException occurred somewhere
in the called function, without giving any insight into the details of the exception
or how it might be fixed.
Sometimes this is okay if the exception passes back obvious information.
Caller Inform
In Caller Inform, additional information is returned for the user. The caught exception
is wrapped in an exception that has additional information.
using System;
public class Summer
{
int sum =0;
int count =0;
float average;
public void DoAverage()
{
try
{
average =sum /count;
}
catch (DivideByZeroException e)
{
//wrap exception in another one,
//adding additional context.
throw (new DivideByZeroException(
"Count is zero in DoAverage()",e));
}
}
}
public class Test
{
public static void Main()
{
Summer summer =new Summer();
try
{
summer.DoAverage();
}
catch (Exception e)
{
Console.WriteLine("Exception:{0}",e);
}
}
}
When the DivideByZeroException is caught in the DoAverage()function, it is wrapped
in a new exception that gives the user additional information about what caused the
exception. Usually the wrapper exception is the same type as the caught exception,
but this might change depending on the model presented to the caller.
This program generates the following output:
Exception:System.DivideByZeroException:Count is zero in DoAverage()--->
System.DivideByZeroException
at Summer.DoAverage()
at Summer.DoAverage()
at Test.Main()
Ideally, each function that wants to rethrow the exception will wrap it in an excep-tion
with additional contextual information.
User-Defined Exception Classes
One drawback of the last example is that the caller can抰 tell what exception hap-pened
in the call to DoAverage()by looking at the type of the exception. To know
that the exception was because the count was zero, the expression message would
have to be searched for the string is zero ".
That would be pretty bad, since the user wouldn抰 be able to trust that the text
would remain the same in later versions of the class, and the class writer wouldn抰
be able to change the text. In this case, a new exception class can be created.
using System;
public class CountIsZeroException:Exceptio
{
public CountIsZeroException()
{
}
public CountIsZeroException(string message)
:base(message)
{
}
public CountIsZeroException(string message,Exception inner)
:base(message,inner)
{
}
}
public class Summer
{
int sum =0;
int count =0;
float average;
public void DoAverage()
{
if (count ==0)
throw(new CountIsZeroException("Zero count in DoAverage"));
else
average =sum /count;
}
}
class Test
{
public static void Main()
{
Summer summer =new Summer();
try
{
summer.DoAverage();
}
catch (CountIsZeroException e)
{
Console.WriteLine("CountIsZeroException:{0}",e);
}
}
}
DoAverage()now determines whether there would be an exception (whether count
is zero), and if so, creates a CountIsZeroException and throws it.
Finally
Sometimes, when writing a function, there will be some cleanup that needs to be
done before the function completes, such as closing a file. If an exception occurs,
the cleanup could be skipped:
using System;
using System.IO;
class Processor
{
int count;
int sum;
public int average;
void CalculateAverage(int countAdd,int sumAdd)
{
count +=countAdd;
sum +=sumAdd;
average =sum /count;
}
public void ProcessFile()
{
FileStream f =new FileStream("data.txt",FileMode.Open);
try
{
StreamReader t =new StreamReader(f);
string line;
while ((line =t.ReadLine())!=null)
{
int count;
int sum;
count =Int32.FromString(line);
line =t.ReadLine();
sum =Int32.FromString(line);
CalculateAverage(count,sum);
}
f.Close();
}
//always executed before function exit,even if an
//exception was thrown in the try.
finally
{
f.Close();
}
}
}
class Test
{
public static void Main()
{
Processor processor =new Processor();
try
{
processor.ProcessFile();
}
catch (Exception e)
{
Console.WriteLine("Exception:{0}",e);
}
}
}
This example walks through a file, reading a count and sum from a file and using it
to accumulate an average. What happens, however, if the first count read from the
file is a zero?
If this happens, the division in CalculateAverage()will throw a DivideByZero-
Exception , which will interrupt the file-reading loop. If the programmer had
written the function without thinking about exceptions, the call to file.Close()
would have been skipped, and the file would have remained open.
The code inside the finally block is guaranteed to execute before the exit of
the function, whether there is an exception or not. By placing the file.Close()call
in the finally block, the file will always be closed.
Efficiency and Overhead
In languages without garbage collection, adding exception handling is expensive,
since all objects within a function must be tracked to make sure that they are
properly destroyed if an exception is thrown. The required tracking code both
adds execution time and code size to a function.
In C#, however, objects are tracked by the garbage collector rather than the
compiler, so exception handling is very inexpensive to implement and imposes little
runtime overhead on the program when the exceptional case doesn抰 occur.
Design Guidelines
Exceptions should be used to communicate exceptional conditions. Don抰 use them to
communicate events that are expected, such as reaching the end of a file. In the
normal operation of a class, there should be no exceptions thrown.
Conversely, don抰 use return values to communicate information that would
be better contained in an exception.
If there抯 a good predefined exception in the System namespace that describes
the exception condition梠ne that will make sense to the users of the class梪se
that one rather than defining a new exception class, and put specific information
in the message. If the user might want to differentiate one case from others where
that same exception might occur, then that would be a good place for a new excep-tion
class.
Finally, if code catches an exception that it isn抰 going to handle, consider whether
it should wrap that exception with additional information before rethrowing it.