Exception handling is a new feature in Visual Basic .NET. Exception handlers are intended to ultimately replace the On Error Goto construct. Exception handlers provide a more robust means of communicating and managing errors and have been available in more advanced languages like C++ and Object Pascal for several years now.
All exception classes are subclassed from the System.Exception class. The basic exception class contains information similar to that found in the VB6 Err object. The differences are that there is no global exception object always availableexception objects are created and raised when neededand in Visual Basic .NET you can subclass and define your own exception classes.
The Try...Catch block is used to catch and handle errors for which you can provide a resolution. The basic Try...Catch block takes the following form:
Try ' some protected code Catch ' resolution on error End Catch
The protected code goes in the Try part of the block and the resolution part is written in the Catch block. The Try code is always executed and the Catch block is executed only in the event of an error.
VB6 simply shuts down on an unhandled error. In some circumstances it may be okay to shut down the application, but most of the time, it is another aggravation users don't need. Visual Basic .NET shuts down the application and makes an attempt to run a Just-In-Time debugger.
If an error is important enough to shut down your application, exceptions will at least provide you with an opportunity to do so in an organized manner. For example, the unavailability of a database may be a sufficient reason to shut down the application. However, when possible, if you can program a resolution to the problem and retry the code, exception handlers allow you to do that. If the problem doesn't need a resolution, you might simply want to show the error to the user and keep on trucking .
There are two kinds of exception handling. The first is referred to as the exception handling block, and the second is referred to as the resource protection block. Both are exception-handling blocks, with the difference being the keywords used to define the blocks, and their intended uses. The exception handler designed to handle errors uses the Try...Catch...End Try block and the resource protection exception handler uses the Try...Finally...End Try block. These types are demonstrated in the following sub-sections. Listing 3.8 demonstrates a generic Try...Catch block used to protect against division by zero.
Recall that division-by-zero for Doubles yields a return value Infinity. Integer division-by-zero raises a DivideByZeroException.
Listing 3.8 Generic exception handler that catches all exceptions.
1: Public Sub TestGenericException() 2: 3: Dim I As Integer 4: Dim Numerator As Integer = 5 5: Dim Denominator As Integer = 0 6: 7: Try 8: I = Numerator \ Denominator 9: Debug.WriteLine(I) 10: Catch 11: End Try 12: End Sub
Lines 7 through 11 demonstrate a generic Try...Catch exception-handling block. This code can be used where you would have written an On Error Resume Next in VB6. A Catch statement with no code is referred to as a silent exception. (Basically, a silent exception works similarly to Resume Next. The reason the demonstration code contains more than I = 1/0 is because the Visual Basic .NET compiler can catch direct integer division by zero, whereas the VB6 compiler cannot.)
The Try part of the exception handler is simple enough that if we want to report the division-by-zero error, we might write a literal message that indicates what went wrong. To implement notification behavior, add something like MsgBox("Division by zero error") between the Catch and End Try lines of code.
Catching Specific Exceptions
Except when you prefer a silent exception, you will probably want to catch and respond to specific kinds of exceptions. Catching specific exceptions is supported with a slightly modified syntax.
In Listing 3.8 we know that we are anticipating a potential division-by-zero error. If we want to catch that error specifically , we need to make a minor modification to the Catch block to indicate our intentions. Listing 3.9 demonstrates catching a specific type of exception, the DivideByZeroException.
Listing 3.9 A Catch block for a specific exception.
1: Public Sub TestGenericException() 2: 3: Dim I As Integer 4: Dim Numerator As Integer = 5 5: Dim Denominator As Integer = 0 6: 7: Try 8: I = Numerator \ Denominator 9: Debug.WriteLine(I) 10: Catch e As System.DivideByZeroException 11: MsgBox(e.Message) 12: End Try 13: 14: End Sub
The revision from Listing 3.8 to 3.9 is constrained to lines 10 and 11. The Catch statement is a declarator. Thus the revision to the Catch statement as demonstrated on line 10 declares an object of type System.DivideByZeroException and initializes it with a caught exception. The Message property of the exception object is displayed in a message dialog box.
Based on the revised definition of this Catch block, all other exceptions would be ignored. If you want to catch all exceptions, use Catch without an exception type. If you want to catch specific exceptions, list each type of exception you would like to handle with an additional Catch statement. Listing 3.10 demonstrates catching multiple exceptions.
Listing 3.10 Multiple Catch clauses.
1: Public Sub BackupFile(ByVal FileName As String) 2: Dim F As System.IO.File 3: Try 4: F.Delete(FileName + ".bak") 5: F.Copy(FileName, FileName + ".bak") 6: 7: Catch e As System.UnauthorizedAccessException 8: MsgBox(e.Message) 9: Catch e As System.IO.FileNotFoundException 10: MsgBox(e.Message) 11: End Try 12: End Sub
Listing 3.10 demonstrates two Catch clauses. Line 7 catches the UnauthorizedAccessException that might occur when you attempt to delete or modify a read-only file. Line 9 catches the FileNotFoundException that might occur when you try to copy a file that doesn't exist.
It is impossible to include all possible ways in which the preceding code might be written. For example, you could check to see if the files existed before trying to delete and copy them. As a general rule, if an If...Then conditional check can preclude the problem, use the conditional check. However, the exception handler will catch errors you might not have thought of. In the listing you could check to see if the file exists before attempting to delete the file, but what if the file exists and is write-protected? You would still get an UnauthorizedAccessException.
Think of exception handlers as safety nets for high-wire walkers and an If...Then conditional similar to deciding whether or not you should be on the wire in the first place. If you have to be up on the wire, it's a good idea to have a net. All of the second-guessing in the world won't catch you if you fall off the wire but have decided to forego the net.
From Bjarne Stroustrup (Stroustrup 1994), the inventor of C++ and father of object-oriented languages for the PC, we know the following things about the aim of an exception handler:
Summarized, this means that the application of exception handling, like many things in programming, requires a subjective measure of good taste and experience.
Throughout this book I often include a statement or rationale indicating why an exception handler was used for each unique occurrence, or rationale for an exception block in code.
Exceptions are objects. If you elect to raise an exception to indicate an error condition that a procedure is not going to handle, you create an exception object like any other object and throw it.
To create an exception instance and throw it, write code as demonstrated in the following example:
Throw New Exception( "message text" )
Throw is a Visual Basic .NET keyword used similarly to the Raise method in VB6. New invokes the constructor for the exception class, and Exception represents any valid exception class subclassed from System.Exception.
If you want to rethrow an exception in an exception block, you can add the Throw statement all by itself in the Catch block. Throw without an explicit exception object raises the last caught exception.
Catch clauses can have a filter applied that enables you to refine the Catch statement. Consider the example in Listing 3.10. Suppose we only wanted to catch the exception UnauthorizedAccessException if the file exists. We could accomplish this goal by adding a when predicate to the Catch statement:
Catch e As System.UnauthorizedAccessException when F.Exists(FileName + ".bak")
Due to the addition of when, this Catch clause will only catch an UnauthorizedAccessException if the backup file exists (perhaps the file exists but is read-only).
Try...Finally blocks are referred to as resource protection blocks. Although Catch blocks are only invoked upon an exception of the type indicated in the Catch clausekeeping in mind that Catch without an exception predicate catches all exceptionsFinally clauses are always invoked whether there is an exception or not.
Try...Finally blocks are used to ensure that allocated resources are cleaned up. The general order of the Try...Finally statement is illustrated in this algorithmic example:
Allocate resource (for example, open a file) Try Use the resource (for example, add some text to the file) Finally Clean up the resource End Try
In this example, only Try, Finally, and End Try are literals. Demonstrating the Try...Finally block using the StreamWriter class, we could create a StreamWriter, try to write some text, and close the writer in the Finally block. Listing 3.11 demonstrates both StreamWriter and the Try...Finally statement.
Listing 3.11 A resource protection block protecting a file managed by the StreamWriter object
1: Public Sub WriteText(ByVal FileName As String, ByVal SomeText As String) 2: 3: Dim W As New System.IO.StreamWriter(FileName, True) 4: Try 5: W.WriteLine(SomeText) 6: Finally 7: W.Close() 8: End Try 9: 10: End Sub
As described in the algorithm, the StreamWriter is allocated on line 3. Line 5 uses the protected resource to write SomeText, and the Finally block ensures that the file represented by the StreamWriter object is closed.
Try...Catch and Try...Finally blocks may be combined to protect against exceptions and resource corruption. To this end, you may nest or combine exception-handling blocks as needed for the desired effect. Consider Listing 3.11 again. What if you want to catch the UnauthorizedAccessException and ensure that opened streams were actually closed? We can combine an exception block with a resource protection block as demonstrated in Listing 3.12.
Listing 3.12 A Try...Finally block nested inside a Try...Catch block
1: Public Sub WriteText(ByVal FileName As String, ByVal SomeText As String) 2: 3: Try 4: Dim W As New System.IO.StreamWriter("sample.txt", True) 5: 6: Try 7: W.WriteLine("Add some text") 8: Finally 9: W.Close() 10: End Try 11: 12: Catch e As System.UnauthorizedAccessException 13: MsgBox(e.Message, MsgBoxStyle.Critical) 14: End Try 15: 16: End Sub
The outer Try...Catch block catches UnauthorizedAccessException for all of the code in between lines 3 and 12. If we get past line 4, the Try...Finally block will ensure that the open StreamWriter is closed on line 9.
You will see many examples of Try...Catch and Try...Finally blocks throughout this book. As with exception-handling blocks, I will elaborate on the rationale behind Try...Finally blocks where it is useful and not redundant to do so.