Data Points

Exception-handling Techniques

John Papa

Code download available at:DataPoints0403.exe(128 KB)

Contents

Knowing Your Exceptions
Catching Exceptions
Custom Exceptions
Demonstration
Handling the Exceptions
Logging the Exceptions
Wrapping Up

Maintaining consistent exception handling throughout an application written with the Microsoft® .NET Framework can be a big problem if you don't have a solid strategy. Some important exception-handling strategies are to keep the amount of code in the catch block to a minimum (while making it generically applicable), allow for the handling of the numerous types of exceptions, provide a clear way to log exceptions to a file (or to another store such as the Windows® Event Log), and provide a way to present information about an error that occurred to the user interface. All these issues will be discussed in this month's column.

I will start with some basic principles, then I'll move on to the try-catch-finally structure. There are many different types of exceptions, all of which have the basic characteristics of the System.Exception class. I will review how to trap some of these exceptions, such as the SqlException, and how to take advantage of their unique features. Following that, I will demonstrate how to create and deploy a custom exception class.

Knowing Your Exceptions

There are times when it is better to program around exceptions than it is to expect them to occur. Just because you can bandage a paper cut doesn't mean it is a good idea to keep giving yourself paper cuts. In the same sense, you could just close your SqlConnection without testing whether it's open and just let the exception be thrown if it isn't. However, it would be better for performance and code clarity to test first. This sort of decision really depends on the frequency with which you expect the error to occur. If you expect a situation to occur on a regular basis, you should then program for that specific situation.

When specific exceptions are likely to occur, you should make sure that a corresponding catch block has been established. For example, if a database concurrency issue may arise and you've decided to catch it, then a catch block to trap the DBConcurrencyException would be appropriate. While all exceptions will eventually flow down to a catch block that traps the Exception type, it is better to trap the more specific exception in this case. This way the application has access to the most detailed information about the exception that was thrown. It is always a good idea to account for the likely exception scenarios, if not for the detailed information to manage the exception then for the maintenance value.

Catching Exceptions

When trapping exceptions, the catch blocks are evaluated sequentially from top to bottom. So if there is a catch block that traps a DivideByZeroException and then another catch block that traps the generic exception, if a DivideByZeroException is thrown it will be caught and processed in the first catch block, and the code in the second catch block will not execute. However, if an exception of any type other than DivideByZeroException or one that derives from it is thrown, it will be caught by the second catch block. Figure 1 and Figure 2 show how the order of the catch blocks comes into play.

Figure 2 Catching All Other Exceptions

try { int[] a = new int[2]; a[0] = 10; a[1] = 2; int b = a[0] / a[1]; a[2] = b; //-- This line will cause an "index out of range" //-- exception } catch (DivideByZeroException exZero) { //-- Handle the DivideByZeroException } catch (Exception ex) { //-- Handle the Exception //-- The index out of range exception will be caught here }

Figure 1 Handling Specific Exceptions First

try { int[] a = new int[2]; a[0] = 10; a[1] = 0; int b = a[0] / a[1]; //-- This line will cause a "division by zero" //-- exception a[2] = b; } catch (DivideByZeroException exZero) { //-- Handle the DivideByZeroException //-- The index division by zero exception will be caught here } catch (Exception ex) { //-- Handle the Exception }

The code in Figure 1 will throw an exception when you try to divide by zero and will be caught in the catch (DivideByZeroException exZero) block. Figure 2 will throw an exception of type IndexOutOfRangeException. This will be caught in the second catch block since a catch block for this specific exception was not established.

You should use a finally block to clean up resources and run code that should always execute whether or not an exception occurred. The finally block is the appropriate place to put code that makes sure your application leaves the procedure in a tidy state, even in cases when an exception is thrown.

So how many catch blocks should you have? As many as you need, of course. A good rule of thumb is to include a catch block for specific exceptions that cannot be programmed around more efficiently. In addition, a last catch block that traps the all-encompassing and generic Exception will cover all the situations that you did not account for specifically.

There are dozens of exception types in the .NET Framework including various database exceptions. For example, the SqlException traps all exceptions related to the SQL Server™ data provider, the OleDbException traps all exceptions rooted from the OLE DB data provider, and the OracleException traps exceptions from the Oracle data provider. All exceptions are derived from the base Exception class. User-defined exceptions should inherit from the ApplicationException class, which itself inherits from Exception. The ApplicationException class is designed to classify exceptions, particularly the user-defined exceptions that inherit from it, in a different hierarchy than exceptions thrown by the common language runtime (CLR).

Custom Exceptions

User-defined exceptions can be useful when an application-specific exception is needed. For example, they're ideal for handling business rule violations such as a customer exceeding his credit limit. When you create user-defined exceptions there are some guidelines you should follow. One such rule of thumb is that any exception class should end with the word "Exception." This common convention is used as a standard way of identifying excep-tion classes. Another, stricter guideline is that when a custom exception is used in an application which throws the exception to another application, both applications must reference the assembly of the custom exception.

There are other situations in which custom exceptions are handy. They can help you standardize the manner in which your exceptions are handled, logged, and reported. For example, all exceptions can be trapped and used as a starting point to create a custom exception. This is the process of creating and throwing a new exception of one type in response to an exception of another type. The custom exception can contain any information you think would be of use to the calling code. This custom information can be valuable in logging and reporting the cause of the exceptions as well as the path the exception took through the call stack.

Demonstration

A custom exception can be used to effectively bubble up, log, and report exceptions in a consistent manner throughout an application. To demonstrate this, I will walk through a sample ASP.NET application that causes three different types of exceptions and wraps them all in a custom exception object called CustomException. The application contains a single Web Form that will intentionally cause three types of exceptions. Figure 3 shows how the custom exception object presents the division-by-zero exception to the user.

Figure 3 Division-by-zero Exception

Figure 3** Division-by-zero Exception **

The date and time that the exception occurred and the type of exception are displayed on the first line. If the exception that was trapped fell into the generic exception catch block, then the type of the exception is listed as "Exception." The next line reports the error message to the screen. This can contain the original error message or can be overridden to provide a custom error message. The next few lines depict the call stack frames that the exception traveled through. Frame #0 of the call stack represents the procedure where the exception was originally thrown. In Figure 3 the exception occurs in the procedure called TestDivideByZeroMethod2, and which is called from TestDivideByZeroMethod1, which in turn is called from the btnOK_Click event handler. The exception bubbles up through the call stack to the entry point (the top of the stack) which, in this case, is the handler that fires when the button is clicked.

An application-specific exception is thrown, as shown in Figure 4. Notice how the call stack is only one frame deep in this case. The call stack always contains the number of procedures that the exception had to travel through to bubble back up to the entry point.

Figure 4 Application-specific Exception

Figure 4** Application-specific Exception **

Figure 5 shows what happens when a SqlException occurs. The call stack and the message are both presented to the user as before; however, additional information is also displayed. In particular, notice that the SQL error number is displayed as "SQL Error #: 547" and that the name of the stored procedure where the exception originated is also reported; as you can see, it's prDelete_Product. This additional information, available through the properties of the SqlException, was transferred to the CustomException.

Figure 5 SqlException

Figure 5** SqlException **

The information reported back to the user is also optionally logged to the Event Log and to a daily exception log. The information is reported back in the text log, as shown in Figure 6. Trapping the class and procedure that the exception traveled back through can make the debugging process much more efficient. The information regarding where and when the error occurred, what type of error it was, and how it got there is all logged to a centralized file. A daily log file is created and used to store exceptions from that particular day, so the exception log can easily be archived, reviewed, or even destroyed when you see fit.

Figure 6 An Entry in the Exception Log

12/15/2003 12:02:38 AM - SqlException occurred, SQL Error #: 547 --> Stored Procedure: prDelete_Product --> Description: DELETE statement conflicted with COLUMN REFERENCE constraint 'FK_Order_Details_Products'. The conflict occurred in database 'Northwind', table 'Order Details', column 'ProductID'. <-- Frame #2: [WebForm1].[private void btnOK_Click(object sender, System.EventArgs e)] <-- Frame #1: [WebForm1].[public void TestSqlMethod1()] <-- Frame #0: [WebForm1].[public void TestSqlMethod2()]

One way to accomplish this type of call stack is to use the StackTrace and StackFrame objects. However, these objects were not as flexible as this sample application required so I created a separate customized call stack that tracks the class and method where the exception traveled. The CustomCallStack class uses an array of CustomCallStackFrame classes to trap this information, as shown in the following lines of code:

private CustomCallStackFrame[] oCallStackFrames = null;

Instances of the CustomCallStackFrame class can be added to the call stack through the Push method of the CustomCallStack class. Figure 7 shows how the Push method adds a new instance of the CustomCallStackFrame class to the private array. By pushing the frames onto the stack, they can later be retraced to create the custom message that can be logged to the exception file and the Event Log, and displayed on the Web Form.

Figure 7 The Push Method of CustomCallStackFrame

/// <summary> /// Pushes a new frame onto the call stack. /// </summary> public void Push(string sSourceClass, string sSourceMethod) { if (oCallStackFrames == null) { //--------------------------------------------------------------- //-- Create a the array of Frames //--------------------------------------------------------------- oCallStackFrames = new CustomCallStackFrame[1]; } else { //--------------------------------------------------------------- //-- Create a temporary array of Frames, one larger than the //-- existing Frame array //--------------------------------------------------------------- CustomCallStackFrame[] oTempFrames = new CustomCallStackFrame[oCallStackFrames.Length + 1]; //--------------------------------------------------------------- //-- Copy the elements over to the temp Frame array //--------------------------------------------------------------- oCallStackFrames.CopyTo(oTempFrames, 0); //--------------------------------------------------------------- //-- Reset the original Frame array //--------------------------------------------------------------- oCallStackFrames = oTempFrames; //--------------------------------------------------------------- //-- Don't need the temp Frame array anymore //--------------------------------------------------------------- oTempFrames = null; } //---------------------------------------------------------------- //-- Create a new CustomCallStackFrame and put it in the //-- CallStack's array //---------------------------------------------------------------- oCallStackFrames[oCallStackFrames.Length - 1] = new CustomCallStackFrame(sSourceClass, sSourceMethod); }

The CustomException class is the main focus of this sample application as it represents the exception that occurred and stores all of the pertinent data including the information that came from the original exception as well as the customized information (such as the call stack). Of course, the CustomException class could have several custom properties added to it, but in this sample I have included only a few basic characteristics such as Type, StoredProcedure, and SqlErrorNumber. The latter two represent the name of the stored procedure that the error occurred in and the SQL Server-specific error number. If the original exception was not a SqlException, then these properties are never set or used. The Type property represents the type of the original exception.

Handling the Exceptions

So far I've discussed how to create a custom exception class, but one of the key benefits of creating maintainable exception handling structures is to simplify the way that the exceptions are caught. So now I will take a step back into the codebehind file called WebForm1.cs and go to the procedure where one of the exceptions originates: TestDivideByZeroMethod2. This method (shown in Figure 8) causes a division-by-zero exception in the try code block. Notice that there are two catch blocks: the first catches a CustomException and the second catches the generic Exception. This means that if a CustomException is raised, it will enter the first catch block. All other exceptions will enter the second catch block where a new CustomException is created, using the originating Exception object, and then thrown.

Figure 8 TestDivideByZeroMethod2

public void TestDivideByZeroMethod2() { string sMethodName = "[public void TestDivideByZeroMethod2()]"; try { //-------------------------------------------------------- //-- Cause a "divide by zero" exception //-------------------------------------------------------- int[] a = new int[2]; a[0] = 10; a[1] = 0; int b = a[0] / a[1]; } catch (CustomException exCustom) { //-------------------------------------------------------- //-- Push the custom exception on the stack and re-throw it. //-------------------------------------------------------- exCustom.CallStack.Push(this.sClassName, sMethodName); throw(exCustom); } catch (Exception ex) { //-------------------------------------------------------- //-- Create and throw a custom exception. //-------------------------------------------------------- throw(new CustomException("", this.sClassName, sMethodName, ex)); } }

By throwing the CustomException in the catch block, the exception bubbles back up the call stack. But in the calling procedure, it will not be caught in the second catch block (for generic exceptions); rather, it will be caught in the first catch block which catches the specific CustomException object. In short, the first time the exception is caught it is not caught by the CustomException catch block. But as it bubbles back up the call stack, all of its calling procedures will catch it in the CustomException catch block. By following this procedure, the extra customized information contained within the CustomException is retained throughout the exception-bubbling process.

When the exception originates, all you need to do is create an instance of the CustomException class using the original Exception as a starting point, as shown here:

catch (Exception ex) { //-------------------------------------------------------- //-- Create and throw a custom exception //-------------------------------------------------------- throw(new CustomException("", this.sClassName, sMethodName, ex)); }

In the constructor of the CustomException, the name of the class and method where this exception originated is pushed onto the custom call stack. Then this code throws the newly created CustomException back to its calling procedure. All catch blocks that catch the CustomException push the class name and procedure name of the corresponding frame onto the stack and then throw the CustomException back up the chain.

Logging the Exceptions

Thus far, the exceptions have been caught, used to create a custom exception, and thrown back up the call stack. However, at some point it would be advantageous to log the exceptions for debugging purposes. I wouldn't want to review the exception log that contains an entry for every single procedure that an exception traveled through, especially if it occurred 20 or 30 procedures down in the call stack. It would be more efficient to simply log the exception at either the originating point or the point where the code originated (in this case the btnOK_Click event handler).

Figure 9 shows the catch blocks from the btnOK_Click event handler. The first line in each event handler is the same as before, but the second line of each event handler is new. This is where the information from the CustomException is logged to the exception log file, the Windows Event Log, and also optionally displayed on the Web Form. The Log method of the CustomException class is overloaded to accept either two Boolean values that indicate if the CustomException object's message should be logged to a file and/or the Windows Event Log, or to accept the two Boolean values and an asp:Label control in which to display the message. Thus, most catch blocks can contain only one or two lines of code.

Figure 9 Catching Exceptions in btnOK_Click

catch (CustomException exCustom) { //-------------------------------------------------------- //-- Log and display the custom exception. //-------------------------------------------------------- exCustom.CallStack.Push(this.sClassName, sMethodName); exCustom.Log(true, true, lblErrorMessage); } catch (Exception ex) { //-------------------------------------------------------- //-- Create a custom exception, then log and display it. //-------------------------------------------------------- CustomException oEx = new CustomException("", this.sClassName, sMethodName, ex); oEx.Log(true, true, lblErrorMessage); }

The code to log the exception file grabs the path from the web.config file in a custom AppSettings called ExceptionLogPath. It then creates the directory if it does not already exist and formats the name of the exception log file using the current date. Finally, the error message, which has been formatted for the file, is written to the exception log file. The code to write the exception to the log file is shown here:

string sPath = ConfigurationSettings.AppSettings.Get("ExceptionLogPath"); if (!System.IO.Directory.Exists(sPath)) { System.IO.Directory.CreateDirectory(sPath); } string sPathAndFile = sPath + "\\" + System.DateTime.Now.ToString(this.sDateFormat) + "-Exception.log"; TextWriter oTR = File.AppendText(sPathAndFile); oTR.WriteLine(sNewMessage); oTR.Close();

Logging to the event log is similar, yet simpler than logging to the exception log file. The following code makes use of the System.Diagnostics namespace's EventLog class, which makes it easy to log application events to the Windows Event Log:

string sEventLogMessage = sNewMessage.Replace(this.sDefaultSpacer, "\n"); sEventLogMessage = sEventLogMessage.Replace(this.sStackSpacer, "\n"); EventLog.WriteEntry("Exception", sEventLogMessage, System.Diagnostics.EventLogEntryType.Error);

Wrapping Up

In .NET Framework-based application development, return values and error codes should usually not be used to report error conditions; either built-in or custom exceptions should be used in these situations. However, you should be careful not to overuse exceptions either. Exceptions should only be thrown and trapped in scenarios where something unanticipated happened, not when you expect it to happen. Understanding exceptions is not rocket science, but knowing how and when to use them takes some degree of planning on your part.

You can find the complete code sample, including the stored procedures that the application attempts to use, at the link at the top of this article.

Send your questions and comments for John to  mmdata@microsoft.com.

John Papa is a baseball fanatic who spends most of his summer nights rooting for the Yankees with his two little girls, wife, and faithful dog, Kadi. He has authored several books on ADO, XML, and SQL Server and can often be found speaking at industry conferences such as VSLive. You can reach him at data@lancelotweb.com.