Don't Commit Errorcide, Part II
Throwing New Exceptions
Continuing the preceding example, we can also catch an exception, log it, wrap the exception in a more meaningful outer exception, and propagate the new exception. Suppose, for example, that you write some code to read a row from a datasource. Internally, you might throw a RowNotInTableException at the locale of the datasource read, but your end uses may want to see a more meaningful exception. To propagate a more meaningful exception, we can use a different exception or create a custom exception and propagate an instance of our custom exception.
To demonstrate this technique, we will play pretend for a moment. Imagine that the next code listing contains a data access layer that is employed by an outer business domain layer. The business layer uses the data layer for persistence. If we try to find data that doesn't exist in the data layer, we throw an exception that is relevant to that layer. If the exception bubbles to the business layer, we can wrap the exception into a more meaningful exception for that layer and propagate the exception. Here is the pseudo-code listing; assume that each method is in a separate assembly.
' Data Access Layer Public Function ReadRow() As IDataReader ' if read succeeds then return instance of IDataReader Throw New RowNotInTableException("Customer") End Function ' Business Layer Public Function GetCustomer() As Customer Try ' read raw object from data access layer IDataReader reader = ReadRow() ' Re-constitute customer and return it Catch(ex As RowNotInTableException) EventLog.WriteEntry("Application", ex.Message) Throw New CustomerNotFoundException("Customer not found", ex) End Try End Function
You can over do logging, wrapping, creating custom exceptions, and propagating exceptions. You are encouraged to download Rotor (the .NET base class libraries) and see how frequently the Microsoft developers throw, catch, log, wrap, and propagate exceptions. Try to get by with a modest amount of structured exception handling code; you can always add more.
Creating Custom Exception Classes
A custom exception is simply a class that inherits from System.Exception or one of its descendants. Microsoft considers it a best practice to inherit from the ApplicationException for custom exceptions. (Best practices are guidelines, not law.) The basic new custom exception needs three constructors and the Serializable attribute, and often not much more. Here is an example of the CustomerNotFoundException class.
<Serializable()> Public Class CustomerNotFoundException Inherits System.ApplicationException Public Sub New() MyBase.New() End Sub Public Sub New(ByVal Message As String) MyBase.New(Message) End Sub Public Sub New(ByVal Message As String, _ ByVal InnerException As Exception) MyBase.New(Message, InnerException) End Sub End Class
The SerializableAttribute—even though this is the full name of the class, we drop the Attribute suffix by convention—permits the exception to be serialized and propagated across process boundaries. The Inherits statement indicates the immediate ancestor of the exception class. (We are following the recommended practice of inheriting from System.ApplicationException here.) The three basic Sub New methods—called constructors—support the constructors we inherited from the parent class. We can add more constructors, methods, and properties if we want to, but we don't need to.
A reasonable person might say, hey, this class doesn't really do anything. The response is discovered in nuance. One of the things that exceptions do is typify errors. Instead of less meaningful numbers, an exception class is a named entity that conveys self-contained meaning and state. For example, the name of the class CustomerNotFoundException tells us exactly what the error is. The type of the class is something we can check for, as we do in the Catch statement. In addition, exception classes contain internal Windows API style error codes for compatibility with COM, a string message indicating the nature of the error, links to help files, and in the case of the ApplicationException, the call stack trace. One just cannot do this with plain vanilla integers.
Exiting a Try Block
Before we wrap up, let's revisit the Try..Finally block. William Opdike wrote a groundbreaking dissertation in 1990 that was reported in Martin Fowler's excellent book, Refactoring: Improving the Design of Existing Code. Popularly called Refactoring, this skill describes rules that remove some of the subjectivity surrounding what constitutes how code is written. One rule is to avoid temporary variables; yet in the presence of the Try..Finally block, temporary variables are still used all of the time to return data after the Finally block. Here is a pseudo-code example (that you should not emulate).
Public Function GetCustomer() As Customer Dim MyCustomer As Customer = Nothing Dim reader As IDataReader Try ' initialize the IDataReader ' MyCustomer = New Customer ' initialize the customer object Finally ' close the reader End Try Return MyCustomer End Function
In the example, the author correctly attempts to ensure that the reader is closed in the finally block, but unnecessarily uses a temporary variable. We don't need to use a temporary here. We can remove the temporary variable, tighten up this code, and achieve the same effect. Here is a preferable revision.
Public Function GetCustomer() As Customer Dim reader As IDataReader Try ' initialize the IDataReader Return New Customer( ' initialized inline ) Finally ' close the reader End Try End Function
The revision completely removes the temporary variable and returns the initialized Customer object in the Try block. The Finally block is still run, even though it may not appear to be, and we have eliminated the unnecessary temporary Customer variable.
This style of code doesn't apply just to IDataReaders; it applies to any resource protected by a Try Finally block. You can have a return statement in the Try block, and the Finally block will still always run. The revision shown in the second example is preferred and conveys to the reader a thorough understanding of structured exception handling.
Structure exception handling can introduce errors. Code is written every day that throws an exception and catches it in the same method. This is a tad schizophrenic: Is your code raising an alarm or responding to one? You will probably not find methods that both throw and catch exceptions anywhere in the BCL; it just doesn't make sense. Code exists that opens a database connection without a resource protection block—Try..Finally—and throws an exception right out of the method, past the connection close statement. ADO.NET is disconnected, unless of course, the programmer throws an exception right over top of the close method call.
When writing structured exception handling, less is more. Read and write a lot of code, especially code written by people whom you recognize as having more experience than you do. A great source for learning is the .NET base class libraries themselves. Don't be afraid to experiment, but at least do no harm.
About the Author
Paul Kimmel has written several books on Visual Basic, including the recently released Visual Basic .NET Power Coding from Addison Wesley. Pick up your copy at www.amazon.com. Paul is also available for short- and long-term consulting, public speaking, and .NET training. You may contact him at email@example.com.
# # #
Page 2 of 2