WEBINAR: On-demand webcast
How to Boost Database Development Productivity on Linux, Docker, and Kubernetes with Microsoft SQL Server 2017 REGISTER >
In this article, we will look at catching specific exceptions, protecting resources, rules for exiting a Try Block, catching multiple exceptions, re-throwing and wrapping exceptions, and creating custom exception classes. Having mastered the mechanics after reading this article, you will find it much easier to decide which facet of structured exception handling to use, or whether to use any at all, in any given circumstance. It is this balance that we are ultimately working toward.
Catching a Specific Exception
An exception is a class, is a class, is a class. Instances of classes—objects—are created with the new operator and constructors, and constructors can accept a variety of parameters because overloaded constructors are supported. The notable difference is that instances of exception classes are almost universally juxtaposed with the Throw or Catch keyword. Instead of returning an error code, we throw an exception; instead of evaluating an error code prior to executing code, we catch an exception only in the presence of an error.
Try ' to do something Catch(ex As Exception) ' perform some cleanup End Try
The instance is on the lefthand side of the As operator and the class of the exception is on the righthand side of the As operator. The higher up in the Exception class hierarchy the type of the class, the more kinds of exceptions a particular block will catch. The example exception handler will actually catch all exceptions, and except for the fact that we have an instance of the exception, the example Catch block works like a Catch block with no exception class indicated. If you want to eliminate some classes of exceptions, pick an exception class farther down in the hierarchy. For example, if you only want to Catch an error that occurs when a file can't be found, the type of the exception in the Catch clause would be FileNotFoundException, and all other exceptions would be ignored.
If one wanted to handle a situation where more than one kind of exception might occur, we would use multiple Catch statements. We'll talk about this more in the next section, Catching Multiple Exceptions.
Catching Multiple Exceptions
If you want to catch multiple exceptions, list multiple Catch blocks with a specific exception class in each Catch clause. The exception classes are evaluated in the order in which they appear, so if you place a more generic exception class before a more specific class, the generic class will catch the exception before the specific Catch block has an opportunity to respond. Using the FileNotFoundException in conjunction with a generic exception class the Try and Catch blocks would be written as follows:
Try ' to do something related to file io Catch(ex As FileNotFoundException) ' respond to the file not being found only Catch(ex As Exception) ' respond to a generic exception End Try
In the example, if we tried to perform an operation that threw a FileNotFoundException, our first Catch clause would respond. Any other exception in the Try block and the second Catch block would respond.
The question is not whether we can catch multiple exceptions, but should we. This is a difficult question and the answer falls into the realm of subjective opinion. That said, the answer is no almost all of the time. If you find yourself writing a lot of methods with multiple catch blocks, your methods are doing too much. I resolved upon this decision after evaluating a few considerations. The reasons include: A method should be named using a noun and verb and that name should describe a singular activity; conventional wisdom suggests that monolithic, multi-function methods are harder to build bug-free and harder to maintain; and finally, an examination of the .NET BCL (base class library) illustrates an absence of multiple catch blocks. This triad of supporting evidence is enough to help permit making a decision and moving on.
Re-Throwing an Exception
A general rule about exceptions is if you are not going to respond to the exception, don't catch it. However, respond has several connotations. One nuance of responding is to log the exception to ensure that errors are recorded in case later troubleshooting is needed.
Suppose you have a requirement to log all unhandled exceptions in the event they result in application failure. Well, if we just catch the exception and log it, the application will think it has been handled. Yet, if are only catching the exception to record it, we need to re-throw it to give some constituent further up the call stack an opportunity to handle the exception. Catching, logging, and propagating an exception could be written as demonstrated.
Try ' Simulate an error Throw New Exception("something is very very wrong here") Catch ex As Exception EventLog.WriteEntry("Application", ex.Message) Throw End Try
In the Try block, we intentionally throw an exception to simulate an error. In the Catch block, we write the exception to the event log using the shared method WriteEntry of the EventLog class and then use the Throw statement. Throw without a specific exception object re-throws an exception in the current context. In the example, this is the exception in the current catch block. Catching an exception to record it is a legitimate partial response, but if we don't re-throw the exception, it is considered handled, which is a good precursor to errorcide.
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
The example supposes that we have a method that performs the persistence part of storing and retrieving objects using ADO.NET. (We use the IDataReader if we want to change data providers; for example, written as demonstrated, the code will work with SQL Server or an OLEDB provider such as Sybase.) If the data access layer can't find the row, it doesn't know what to do, so it signals an error by throwing the RowNotInTableException. In the business layer, we want to catch this exception, perhaps log it, and then wrap it in a custom exception. The custom exception may be something that is more meaningful in the business domain, and we keep track of the original error by initializing the new exception with the old exception.
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 firstname.lastname@example.org.
# # #