Error handling code should make your application better but never introduce problems. In Part 1 of this two-part article, I will demonstrate the syntactical concepts of structured exception handling and how a "less is more" approach will help you make your applications more robust without introducing new errors.
Error handling is still a pretty subjective art form. It is a challenging prospect to prove that an application has too little or too much structured exception handling code. The art works well when exception handling code is applied in conjunction with problem-solving code and then is fine tuned to remediate specific problems during testing and maintenance. After reading this article, you will have a better understanding of the technical aspects of using structured exception handling, and the art will take practice. In the interim, your applications should not commit errorcide.
Basic Exception Handling Syntax
VB.NET uses a class to convey information about the type of the error. Instead of a single, one-size fits all Err object, each class of error can be captured by a specific class. This permits developers to extend the error classification system to suit it for each problem domain. The number of types is unlimited. For example, if your application must know the identity of the operator or the thread the exception occurred on, the developer can implement such a class of error by sub-classing an existing exception base class. For a complete examination of the differences between VB6 structured errors and VB.NET structured exceptions, we would need another whole article. We'll leave further examination of the comparisons perhaps for another day or venue.
To begin using structured exceptions, it will be helpful to review the basic grammar of the fundamental, recommended exception handling code in VB.NET. (It is worth noting that VB6-style error handling is supported in .NET, but I suspect this will ultimately go the way of the dinosaur.) Listing 1 demonstrates the basic structure of an exception handling block.
Listing 1: A basic example of an exception handling block.
Try File.Delete("B:\FILE.NOT.EXISTS") Catch Console.WriteLine("Error") End Try
In the example, the code tries to delete a file on the B: drive. (Actually, deleting a non-existent file on a valid drive does not raise an exception. It is an acceptable operation.) The basic behavior here is that the code between Try and Catch is tried. If something goes wrong, the code between Catch and End Try runs and the error is considered handled (in the scenario shown). Think of the Try block as the man on the high-wire at the circus and the Catch block as the net below. If something goes wrong, it is nice to have that net.
Other forms of exception handlers include Try and Finally, Try, Catch, and Finally, and Try, more than one Catch, and optionally a Finally. We'll cover these variations in this article. It is important to understand at this juncture that every subroutine or function does not need, nor should have, all or even any of these elements. Some subroutines and functions will benefit from structured exception handling, and in some cases it is added noise. When to use these features is a subjective measure of good taste and is the subject of the rest of this article.
Implementing the Try Block
The Try Finally block is referred to (by some) as a resource protection block. Try Finally without a Catch block takes the following form:
Try // do something that needs cleanup Finally // cleanup here, always! End Try
The Finally block is called a resource protection block because that is its purpose. Code in the Finally blocks is always run. Hence, if you need to do something such as ensure a file, socket, or database connection is closed, you want to use the Try Finally construct. The basic rhythm of the Try Finally construct is demonstrated with code and comments next:
// create a resource that you want to protect Try // try to use the protected resource Finally // always clean up the protected resource End Try
The example in Listing 2 demonstrates the rhythm of the resource protection block. In the listing, a text file is created. In the Try block, we attempt to write something to the text file. Whether the write succeeds or fails, the Finally block is executed and the file is closed.
Listing 2: A Try Finally block protecting the finite number of file handles.
Public Sub FinallyHandler() Dim MyTextFile As TextWriter = File.CreateText("some.txt") Try MyTextFile.WriteLine("Welcome to Valhalla Tower Material Defender!") Finally MyTextFile.Close() End Try End Sub
The reason the resource is created outside of the Try block is two-fold. First, if we can't even create the file, an error occurs and we never enter the Try block. In the first scenario, the resource isn't created so there is no need to protect, and, trying to protect an uncreated resource can itself cause errorcide. In the example, if we declared the TextWriter outside of the Try block but created the instance inside of the Try block and the file could not be created, we get an error indicating the file couldn't be created and an error when we try to close the un-initialized TextWriter, resulting in death by error handler, or errorcide. The second reason the resource is declared outside of the try block is that the Try and Finally blocks have individuated scope. Variables declared in Try cannot be seen in Finally. Yet the subroutine in the example has an outer, containing scope and the Try and Finally blocks have inner scope. Something in the subroutine's outer scope—our resource to be protected—can be seen in the inner scope of both Try and Finally.
What happens in our example if we don't use the Finally block? The answer is that there is no guarantee that the TextWriter.Close method is called. Consequently, you may find that the text file appears to be empty. This is because closing the file flushes—programming vernacular is so colorful—execute, flush—it, ensuring that all of the text is actually written, and that the file is now available to some other process.
Another mistake that seems to be commonly made is declaring a temporary variable and placing the return statement after the Try Finally block. Suppose we wanted to read something from our text file and return that. Listing 3 shows this example with the mistakenly placed Return statement.
Listing 3: Don't use temporary variables and an extra-try block return statement.
Public Function GetText() As String Dim MyTextFile As TextReader = File.OpenText("some.txt") Dim Temp As String Try Temp = MyTextFile.ReadLine() Finally MyTextFile.Close() End Try Return Temp End Function
The preceding example is grammatically correct. Assuming some.txt exists and contains some code, GetText will read and return that text. Applying subjective rules relative to innovations in structured exception handling and Refactoring, we can and should tighten up the code as demonstrated in Listing 4.
Listing 4: A refactored, tighter implementation of GetText.
Public Function GetText() As String Dim MyTextFile As TextReader = File.OpenText("some.txt") Try Return MyTextFile.ReadLine() Finally MyTextFile.Close() End Try End Function
Temporary variables usually clutter up code without adding any significant value. Martin Fowler's book Refactoring: Improving the Design of Existing Code provides reasonable arguments against using spurious random temporary variables, and the Finally block does not require it. The code looks as if it might exit at the Return statement (in bold font), but remember the Finally block is always run. Thus, even with an internal statement, we can easily step through the code and determine that the Return statement executes, followed by the code in the Finally block. Beneficially, our code is simplified by about a third and appears to be less cluttered. Functionally, the code in Listing 4 is equivalent to the code in Listing 3.
Catching an Exception
When do we catch an exception? The answer is a bit of a slippery slope. We should catch an exception if we can handle the error at the locus of the catch block. And, we can catch an exception if we want to log the exception and propagate it, permit silent errors to occur, catch the exception, degrade, and shut down, or catch an exception, wrap it in a new exception, and propagate the new exception. Technically, you can catch an exception for any reason you want, but it is a waste of code to catch an exception that you can or should do nothing about.
The following listings demonstrate each of the techniques mentioned in the preceding paragraph. The listing headers describe the technique demonstrated.
Listing 5: Handling an exception.
Public Sub HandlingAnException() Try Process.Start("http://www.softconcepts.com") Catch Console.WriteLine("Can't load Web page") End Try End Sub
In the example, we catch an exception that might occur when we try to browse to a Web site. Handling in this context might be as simple as telling the user that the Web page cannot be loaded. In the example, this permits the application to continue and provides the user with enough information to manually resolve the problem. If we can automatically resolve the problem, that is an even better solution.
Listing 6: Catching an exception, degrading, and shutting down.
Public Sub HandleAndShutdown() Try Console.WriteLine(GetText()) Catch Shutdown("There is a problem with the file.") End Try End Sub Private Sub Shutdown(ByVal Message As String) Console.WriteLine(Message) Console.WriteLine("press enter to exit") Console.ReadLine() End End Sub
Listing 6 tries to use the GetText method to read the text file. If something goes wrong, we tell the user and shut down the application. The End statement in the sample terminates the console application immediately. (Use Application.Exit instead of End for a Windows application.) Shutting down gracefully is another example of a contextually subjective concept. The application suggested by Listing 6 is a simple text-reading solution. If the text file to read doesn't exist, the user needs to intercede and try again. In a real world application, degrading and shutting down will likely be more involved but the result is the same: Instead of a shocking blue screen of death, the user is pleasantly prepared for the shutdown and has a chance to resolve the problem.
Listing 7: Logging an exception and propagating it.
Imports System.Diagnostics Public Sub LogAndPropagate() Try Process.Start("http://www.softconcepts.com") Catch EventLog.WriteEntry("codeguru.com", "Can't load web page.") Throw End Try End Sub
In Listing 7, the programmer decided that the error should be logged but further handling cannot be accomplished in the current context. To permit an outer context—the caller—an opportunity to resolve the problem, the code re-throws the original exception after logging a record of the error in the EventLog.
Listing 8: Silent exception.
Public Sub SilentException() Try File.Delete("b:\google.txt") Catch End Try End Sub
Listing 8 implies that the implementer does not care whether the file is deleted or not. The algorithm that produced such a procedure might read: Delete the file b:\google.txt if it's not too much trouble; it isn't that important, but don't fail.
Listing 9 demonstrates how to create a custom exception class, catch, wrap, and throw a new exception. The class BetterException represents a domain-specific class of error. This might be a class that is meaningful to your application. The first bold statement containing Throw New simulates an error occurring in the Try block of a specific method. The catch block demonstrates how to catch any exception that might occur and wrap it in a new, domain-specific exception, throwing that new exception.
Listing 9: Wrapping an exception and propagating a new exception.
Public Class BetterException Inherits System.Exception 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 Public Sub WrapAndPropagate() Try Throw New Exception("Some error") Catch ex As Exception Throw New BetterException("This is more meaningful", ex) End Try End Sub
Consider a contrived scenario. Your application performs currency conversions. You attempt to open a file containing the daily currency conversion rates, but the conversion file cannot be found. It is likely that a FileNotFoundException will occur. At the location you attempt to read such a file, this is a valid exception. However, in the domain of currency conversions, it may be more meaningful to wrap the FileNotFoundException in a CurrencyConversionException as the inability to convert the currency conveys more meaning to end users. The outer CurrencyConversionException lets the user know the request to convert between currencies failed, and the inner FileNotFoundException helps a diagnostician resolve the problem. (See Creating Custom Exceptions, in Part II of this article for more information on defining custom exceptions.)
In Part I of this article on preventing errorcide, we talked about the grammar of structured exception handling, including how to use Try, Catch, and Finally blocks to catch errors and protect resources. We also talked about some of the different error handling techniques we might employ, including logging, wrapping, and propagating exceptions. These fundamental exception handling capabilities are essential to writing robust, efficacious, software.
In Part II, we will examine more closely how to catch multiple exceptions, re-throw exceptions, when and how to create custom exceptions, and how to eliminate excessive exception handling that is at the root of errorcide.
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 and .NET training. You may contact him at email@example.com.
# # #