Exception Handling in C#

In many programming books, exception handling warrants a chapter somewhat late in the book. In this book, however, it’s near the front for a couple of reasons.

The first reason is that exception handling is deeply ingrained in the .NET runtime and is therefore common in C# code. C++ code can be written without using exception handling, but that’s not an option in C#.

The second reason is that it allows the code examples to be better. If exception handling is late in the book, early code samples can’t use it, and that means the examples can’t be written using good programming practices.

Unfortunately, this means classes must be used without really introducing them. Read the following section for flavor; we’ll cover the classes in detail in the next chapter.

1. What’s Wrong with Return Codes?

Most programmers have probably written code that looks 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 previous code was written as

CallFunction();

any error return would be thrown away. That’s where bugs come from.

Many different models exist 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 handling error conditions. Exceptions are nicer than return codes because they can’t be silently ignored.

2. 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’s 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 DivideByZeroException. 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’s contained within the exception object.

3. The Exception Hierarchy

All C# exceptions derive from the class named Exception, which is part of the CLR. 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. Let’s return 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’s executed. catch blocks always must be listed from most specific to least specific, so in this example, the two blocks couldn’t be reversed.

 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’t 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’t 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’s 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 exception information and halt the program.

4. Passing Exceptions on to the Caller

Sometimes you can’t do much when an exception occurs; it really has to be handled by the calling function. You have three basic ways to deal with this, which are named based on their result in the caller: Caller Beware, Caller Confuse, and Caller Inform.

4.1. 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.

4.2. 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;

}

}

}

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 although the object is in a valid state alter 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.

5. Caller Inform

In Caller Inform, additional information is returned for the user. The caught exception is wrapped in an exception that has additional information. For example:

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’s 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 exception with additional contextual information.

6. User-Defined Exception Classes

One drawback of the last example is that the caller can’t tell what exception happened 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 Count is zero.

That would be pretty bad, since the user wouldn’t be able to trust that the text would remain the same in later versions of the class, and the class writer wouldn’t be able to change the text. In this case, a new exception class can be created:

using System;

public class CountIsZeroException: ApplicationException {

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.

In this example, the exception class has three constructors, which is the recommended design pattern. It’s important to follow this design pattern because if the constructor that takes the inner exception is missing, it won’t be possible to wrap the exception with the same excep­tion type; it could be wrapped only in something more general. If, in the previous example, the caller didn’t have that constructor, a caught CountIsZeroException couldn’t be wrapped in an exception of the same type, and the caller would have to choose between not catching the excep­tion and wrapping it in a less-specific type.

Also notice that the exception class is derived from ApplicationException, which is the base of application-derived exceptions and therefore should be used for all exceptions defined in an application.

7. Finally

Sometimes, when writing a function, you’ll need to do some cleanup 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 = Convert.ToInt32(line);

line = t.ReadLine();

sum = Convert.ToInt32(line);

CalculateAverage(count, sum);

}

}

// 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 accu­mulate 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 DivideByZeroException, 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 or not there is an exception. By placing the file.Close() call in the finally block, the file will always be closed.

8. 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 they’re properly destroyed at any time that an exception could be thrown. The required tracking code adds both 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 inexpensive to implement and imposes little runtime overhead on the program when the exceptional case doesn’t occur.

9. Design Guidelines

You should use exceptions to communicate exceptional conditions. Don’t use them to communi­cate events that are expected, such as reaching the end of a file. In the normal operation of a class, no exceptions should be thrown.

Conversely, don’t use return values to communicate information that would be better contained in an exception.

If a good predefined exception in the System namespace describes the exception condition— one that will make sense to the users of the class—use that one rather than defining a new exception class, and put specific information in the message. If the user might want to differ­entiate one case from others where that same exception might occur, then that would be a good place for a new exception class.

Finally, if code catches an exception that it isn’t going to handle, consider whether it should wrap that exception with additional information before rethrowing it.

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *