Don't Commit Errorcide, Part I, Page 2
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
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.
# # #
Page 2 of 2