CHAPTER 5

image

Exception Handling

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 few reasons.

The first reason is that exception handling is deeply ingrained in the .NET Runtime and is therefore very 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 presented late in the book, early code samples can’t use it, and that means the examples can’t be written using good programming practices.

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 above was written as

CallFunction();

any error return would be thrown away. That’s 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 handling error conditions. Exceptions are nicer than return codes because they can’t be silently ignored. Or, to put it another way, the error handling in the .NET world is correct by default; all exceptions are visible.

image Note  In practice, this means that any exception code you write just gives you the opportunity to mess up that “correct-by-default” behavior. You should therefore be especially careful when writing exception handling code, and, more importantly, strive to write as little as possible.

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. It then looks for a catch block and writes out the exception message.

All C# exceptions inherit from a class named Exception. For example, the ArgumentException class inherits from the SystemException class, which inherits from Exception.

Choosing the Catch Block

When an exception occurs, the matching catch block is determined using the following approach:

  1. The runtime searches for a try block that contains the code that caused the exception. If it does not find a try block in the current method, it searches the callers of the method.
  2. After it finds a try block, it checks the catch blocks in order to see if the type of the exception that was thrown can be converted to the type of exception listed in the catch statement. If the conversion can be made, that catch block is a match.
  3. If a matching catch block is found, the code in that block is executed.
  4. If none of the catch blocks match, the search continues with step 1.

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 first match and is therefore the one that is executed. Catch blocks always must be listed from most specific to least specific, so in this example, the two blocks couldn’t be reversed.1

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 block in Main(). It finds one, and then looks for a catch block 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 might write out the exception information before the program exits.2

Passing Exceptions on to the Caller

It’s sometimes the case that there’s not much that can be done when an exception occurs in a method; 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 usually the right design decision, but it could leave the object in an incorrect state, causing problems if the caller tries to use it later. It may also give insufficient information to the caller to know exactly what has happened.

Caller Confuse

The second way is to catch the exception, do some cleanup, and then rethrow the exception:

using System;
public class Summer
{
    int m_sum = 0;
    int m_count = 0;
    float m_average;
    public void DoAverage()
    {
       try
       {
            m_average = m_sum / m_count;
       }
       catch (DivideByZeroException e)
       {
            // do some cleanup here
            throw; //rethrow the exception
       }
    }
}
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.

If the information in the exception is sufficient for the caller to understand what has happened, this is the preferred behavior.

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 m_sum = 0;
    int m_count = 0;
    float m_average;
    public void DoAverage()
    {
       try
       {
            m_average = m_sum / m_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 Test.Main()

If wrapping an exception can provide useful information to the user, it is generally a good idea. However, wrapping is a two-edged sword; done the wrong way, it can make things worse. See the “Design Guidelines” section later in this chapter for more information on how to wrap effectively.

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 caused because the count was zero, the expression message would have to be searched for using 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: Exception
{
    public CountIsZeroException()
    {
    }
    public CountIsZeroException(string message)
    : base(message)
    {
    }
    public CountIsZeroException(string message, Exception inner)
    : base(message, inner)
    {
    }
}
public class Summer
{
    int m_sum = 0;
    int m_count = 0;
    float m_average;
    public void DoAverage()
    {
       if (m_count == 0)
       {
            throw(new CountIsZeroException("Zero count in DoAverage()"));
       }
       else
       {
            m_average = m_sum / m_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 is 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 exception type; it could only be wrapped in something more general. If, in the above example, our 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 exception and wrapping it in a less-specific type.

In earlier days of .NET, it was recommended that all user-defined exceptions be derived from the ApplicationException class, but it is now recommended to simply use Exception as the base.3

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. The following code processes a file:

using System;
using System.IO;
class Processor
{
    int m_count;
    int m_sum;
    public int m_average;
    void CalculateAverage(int countAdd, int sumAdd)
    {
       m_count += countAdd;
       m_sum += sumAdd;
       m_average = m_sum / m_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 accumulates 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.

The code in the previous example is a bit clunky. Chapter 7 covers the using statement, which is often used to make dealing with resource cleanup simpler.

Top-Level Exception Handlers

If our program encounters an exception and there is no code to catch the exception, the exception passes out of our code, and we depend on the behavior of the caller of our code. For console applications, the .NET Runtime will write the details of the exception out to the console window, but for other application types (ASP.NET, WPF, or Windows Forms), our program will just stop executing. The user will lose any unsaved work, and it will be difficult to track down the cause of the exception.

A top-level exception handler can be added to the catch the exception, perhaps allow the user to save his or her work,4 and make the exception details available for troubleshooting.

The simplest top-level handler is a try-catch in the Main() method of the application:

static void Main(string[] args)
{
    try
    {
       Run();
    }
    catch (Exception e)
    {
       // log the exception, show a message to the user, etc.
    }
}

For a single-threaded program, this works fine, but many programs perform operations that do not occur on the main thread. It is possible to write exception handlers for each routine, but it’s fairly easy to do it incorrectly, and other threads will probably want to communicate the exception back to the main program.

To make this easier, the .NET Runtime provides a central place where all threads go to die when an unhandled exception happens. We can write our top-level exception-handling code once and have it apply everywhere:

static void Main(string[] args)
{
    AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler;
    int i = 1;
    i--;

    int j = 12 / i;
}
static void UnhandledExceptionHandler (object sender, UnhandledExceptionEventArgs e)
{
    Exception exception = (Exception) e.ExceptionObject;

    Console.WriteLine(exception);
    System.Diagnostics.Debugger.Break();
}

The first line of Main() connects the event handler UnhandledExceptionHandler to the UnhandledException event on the current application domain.5 Whenever there is an uncaught exception, the handler will be called.

The handler writes the message out to the console window (probably not the best thing to do in real code, especially code that does not have a console window), and then, if the debugger is attached, causes a breakpoint to be executed in the debugger.

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 are properly destroyed at any time 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 very inexpensive to implement and imposes little runtime overhead on the program when the exceptional case doesn’t occur. It is, however, not cheap when exceptions are thrown.

Design Guidelines

The following are design guidelines for exception usage.

Exceptions Are Exceptional

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

image Tip  If you are writing C# using Visual Studio, the debugger exceptions window allows you to set up the debugger to break whenever an exception is thrown. Enabling this option is a great way to track whether your program is generating any unexpected exceptions.

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

Choosing the Right Exception for Wrapping

It is very important to make the right choice when wrapping exceptions. Consider the following code:

try
{
    libraryDataValue.Process();
}
catch (Exception e)
{
    if (e.InnerException is FileNotFoundException)
    {
       Recover(); // Do appropriate recovery
    }
}

Look at that code for a minute and see if you can spot the bug. The problem is not with the code that is written; it is with the code that is missing. Correct code would look something like this:

try
{
    libraryDataValue.Process();
}
catch (Exception e)
{
    if (e.InnerException is FileNotFoundException)
    {
       Recover(); // Do appropriate recovery
    }
    else
    {
       throw;
    }
}

The lack of the else clause means that any exception thrown that does not have an inner exception of type FileNotFoundException will be swallowed, leaving the program in an unexpected state. In this case, we’ve written some code, and we’ve broken the “correct-by-default” behavior.

However, it’s not really our fault. The fault lies in the author of the Process() method, who took a very useful and specific exception—FileNotFoundException—and wrapped it in the very generic Exception type, forcing us to dig into the inner code to find out what really happened. This is especially annoying because FileNotFoundException is a perfectly good exception and doesn’t need to be wrapped in another type.

When considering wrapping exceptions, consider the following guidelines:

  • Evaluate how useful the additional information is going to be. What information would the developer get if the exception wasn’t wrapped, and would that be sufficient? Is the code going to be used by other developers on your team with access to the source (who can therefore just debug into it), or is it an API used by somebody else, where wrapping may be more useful?
  • Determine when this exception is likely to be thrown. If it’s in the “developer made a mistake” class, wrapping is probably less useful. If it’s a runtime error, it’s likely to be more useful.
  • Wrap exceptions at that same level of granularity that they are thrown. Information in the inner exception is there to help debugging, nobody should ever have to write code that depends on it.
  • Wrapping an exception in the same type but with more information in the message is often a good choice.

Exceptions Should be as Specific as Possible

If your code needs to throw exceptions, the exceptions that it throws should have as specific a type as possible. It’s very tempting to just define a SupportLibraryException class and use it in multiple places, but that makes it much more likely that callers have to look inside that class, and they may even use text matching on the exception message text to get the desired behavior. Spend the extra time and give them a different exception for each discrete case. However, if you want to derive all of the specific exceptions from SupportLibraryException, that will make it easy for the caller to write general code if they want to.

Retry Logic

Some time ago, I came across a system that had retry logic at the low level; if it ran into an issue, it would retry ten times, with a few seconds’ wait between each retry. If the retry logic failed, it would give up and throw the exception. And then the caller, which also implemented retry logic as well, would follow the same approach, as did the caller’s caller.

When the system hit a missing file, it kept trying and trying, until it finally returned an exception to the caller, some 15 minutes later.

Retry logic is sometimes a necessary evil, but before you write it, spend some time thinking if there’s a better way to structure your program. If you do write it, also write yourself a note to revisit the code in the future to make sure the retry logic is still useful and behaving the way you expect.

Rethrowing

Code that rethrows an exception that it caught is usually a sign that something is wrong, as noted in the guideline on wrapping exceptions. If you do need to rethrow, make sure you do this:

throw;

rather than this:

throw e;

The second option will throw away the stack trace that was originally generated with the exception, so the exception looks like it originated at the rethrow.

Catch Only if You Have Something Useful to Do

As noted at the beginning of the chapter, writing exception handling code is an opportunity to take a system that works just fine and turn it into one that doesn’t work right. There are three definitions of useful that I’ve come across6:

  • You are calling a method, it has a well-known exception case, and, most importantly, there is something you can do to recover from that case. The canonical example is that you got a filename from a user and for some reason it wasn’t appropriate (didn’t exist, wrong format, couldn’t be opened, etc.). In that case, the retry is to ask the user for another filename. In this case, “something” means “something different.” Retry is almost never the right thing to do.7
  • The program would die if you didn’t catch the exception, and there’s nobody else above you. At this point, there’s nothing you can do except capture the exception information to a file or event log and perhaps tell the user that the program needs to exit, but those are important things to do.
  • You are at a point where catching and wrapping provide a real benefit to your caller. In this case, you’re going to catch the exception, wrap it, and then throw the wrapped exception.

1 More specifically, a clause catching a derived exception cannot be listed after a clause catching a base exception.

2 This behavior can be changed by the application.

3 If you are creating a library, it’s a good idea to define a UnicornLibraryException class and then derive all your specific classes from that one.

4 Saving their work can be problematic, as it’s possible that the exception has left their work in an invalid state. If it has, saving it might be a bad thing to do. If you do decide to save their work, I suggest saving it as a copy.

5 Which references two new concepts. For more on events, see Chapter 23. Application domains can be thought of as the overall context in which code executes.

6 It is possible that I’m missing additional cases. Just be very thoughtful and deliberate before you conclude that what you want to do is, in fact, useful.

7 Say you are writing a mobile phone app that needs to be resilient if it loses network access. If you write logic to keep trying network operations, it will probably work, except that if you use polling you will run the battery down really quickly. The point here being that the correct retry behavior is typically something you need to design in architecturally, not handle at the low level.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.148.107.255