Structured exception handling is based around the idea that while exceptions should be used for unexpected conditions, they can be built within your application structure. Some older languages would allow for generic error handling that didn't exist within a defined set of boundaries. However, professional developers learned long ago that even unexpected conditions should be definable within your application structure.
To allow for this you may have what is known as a last-chance error handler at the topmost level of your application; however, most error handling is structured within individual modules. Within Visual Basic error handling depends on four keywords. Three of these are associated with properly identifying and handling exceptions, while the fourth is used when you wish to signal that an unexpected condition has occurred.
The next section of the chapter covers the keywords in detail and includes code samples of the keywords in action. All the code in this section is included in the code download for this chapter.
Now is a good time to build an example of some typical, simple structured exception-handling code in Visual Basic. In this case, the most likely source of an error will be a division by 0 error.
To keep from starting from scratch, you can take the sample WPF application from Chapter 1 and use it as a baseline framework. Similar to Chapter 1, individual modules (functions) can be created, which are then called from the default button handler. For the purposes of this chapter, the application will have the name ProVB_Ch06.
Start with the following code snippet, the iItems argument. If it has a value of zero, then this would lead to dividing by zero, which would generate an exception.
Private Function IntegerDivide(iItems As Integer, iTotal As Integer) As String Dim result As String ' Code that might throw an exception is wrapped in a Try block Try ' This will cause an exception to be thrown if iItems = 0 result = iTotal iItems Catch ex As Exception ' If the calculation failed, you get here result = "Generic exception caught" & ex.Message End Try Return result End Function
This code traps all exceptions using the generic type Exception, and doesn't include any Finally logic. Before running the program keep, in mind you'll be able to follow the sequence better if you place a breakpoint at the top of the IntegerDivide function and step through the lines. If you pass the value 0 in the first parameter you'll see you get a divide by zero exception. The message is returned.
The next code snippet illustrates a more complex example that handles the divide-by-zero exception explicitly. This code contains a separate Catch block for each type of handled exception. If an exception is generated, then .NET will progress through the Catch blocks looking for a matching exception type. This code helps illustrate that Catch blocks should be arranged with specific types first.
The IntegerDivide2 method also includes a Finally block. This block is always called and is meant to handle releasing key system resources like open files on the operating system, database connections, or other operating-system-level resources that are limited in availability.
Private Function IntegerDivide2(iItems As Integer, iTotal As Integer) As String Dim result As String ' Code that might throw an exception is wrapped in a Try block Try ' This will cause an exception to be thrown if iItems = 0 result = iTotal iItems Catch ex As DivideByZeroException ' You'll get here with a DivideByZeroException in the Try block result = "Divide by zero exception caught: " & ex.Message Catch ex As Exception ' If the calculation failed, you get here result = "Generic exception caught: " & ex.Message Finally MessageBox.Show( "Always close file system handles and database connections.") End Try Return result End Function
Keep in mind when you run this code that the event handler will display the message box prior to updating the main display.
Sometimes a Catch block isn't meant to fully handle an error. Some exceptions should be “sent back up the line” to the calling code. In some cases this allows the problem to be visible to the correct code to handle and possibly even log it. A Throw statement can be used to look for the next higher error handler without fully handling an error.
When used in a catch block the Throw statement ends execution of the exception handler—that is, no more code in the Catch block after the Throw statement is executed. However, Throw does not prevent code in the Finally block from running. That code still runs before the exception is kicked back to the calling routine.
Note when rethrowing an exception you have two alternatives, the first is to simply take the exception you've handled and literally throw it again. The second is to create a new exception, add your own information to that exception, and assign the original exception as an inner exception. In this way you can add additional information to the original exception to indicate where it was rethrown from.
Throw can also be used with exceptions that are created on the fly. For example, you might want your earlier function to generate an ArgumentException, as you can consider a value of iItems of zero to be an invalid value for that argument.
In such a case, a new exception must be instantiated. The constructor allows you to place your own custom message into the exception. The following code snippet illustrates all of the methods discussed related to throwing exceptions. This includes detecting and throwing your own exception as well as how to rethrow a previously occurring exception.
Private Function IntegerDivide3(iItems As Integer, iTotal As Integer) As String If iItems = 0 Then Dim argException = New _ ArgumentException("Number of items cannot be zero") Throw argException End If Dim result As String ' Code that might throw an exception is wrapped in a Try block Try ' This will cause an exception to be thrown if iItems = 0 result = iTotal iItems Catch ex As DivideByZeroException ' You'll get here with a DivideByZeroException in the Try block result = "Divide by zero exception caught: " & ex.Message Throw ex Catch ex As Exception ' If the calculation failed, you get here result = "Generic exception caught: " & ex.Message Dim myException = New Exception("IntegerDivide3: Generic Exception", ex) Throw myException Finally MessageBox.Show("Always close file system handles and database connections.") End Try Return result End Function
Error handling is particularly well suited to dealing with problems detected in property procedures. Property Set logic often includes a check to ensure that the property is about to be assigned a valid value. If not, then throwing a new ArgumentException (instead of assigning the property value) is a good way to inform the calling code about the problem.
The Exit Try statement will, under a given circumstance, break out of the Try or Catch block and continue at the Finally block. In the following example, you exit a Catch block if the value of iItems is 0, because you know that your error was caused by that problem:
Private Function IntegerDivide4(iItems As Integer, iTotal As Integer) As String Dim result As String ' Code that might throw an exception is wrapped in a Try block Try ' This will cause an exception to be thrown if iItems = 0 result = iTotal iItems Catch ex As DivideByZeroException ' You'll get here with a DivideByZeroException in the Try block result = "Divide by zero exception caught: " & ex.Message ' You'll get here with a DivideByZeroException in the Try block. If iItems = 0 Then Return 0 Exit Try Else Throw ex End If Catch ex As Exception ' If the calculation failed, you get here result = "Generic exception caught: " & ex.Message Dim myException = New Exception("IntegerDivide3: Generic Exception", ex) Throw myException Finally MessageBox.Show( "Always close file system handles and database connections.") End Try Return result End Function
In your first Catch block, you have inserted an If block so that you can exit the block given a certain condition (in this case, if the overflow exception was caused because the value of iItems was 0). The Exit Try goes immediately to the Finally block and completes the processing there.
Now, if the overflow exception is caused by something other than division by zero, you'll get a message box displaying “Error not caused by iItems.”
A Catch block can be empty. In that case, the exception is ignored. Also remember, exception handlers don't resume execution with the line after the line that generated the error. Once an error occurs, execution resumes either the Finally block or the line after the End Try if no Finally block exists.
Sometimes particular lines in a Try block may need special exception processing. Moreover, errors can occur within the Catch portion of the Try structures and cause further exceptions to be thrown. For both of these scenarios, nested Try structures are available.
When you need code to resume after a given line of code, or if you are doing something that may throw an error within a catch block, you'll want a nested structured error handler. As the name implies, because these handlers are structured it is possible to nest the structures within any other.
The preceding examples have all leveraged the Message property of the exception class. When reporting an error, either to a display such as a message box or to a log entry, describing an exception should provide as much information as possible concerning the problem.
Because in most cases the output hasn't been the focus of the discussion on structured error handling, you haven't seen many screenshots of the output. Returning to the original example, which displayed an error message, you know that the display using the message property looks similar to what is seen in Figure 6.1.
This is a reasonable error message that explains what happened without providing too much detail. While a professional would normally adjust the wording of this message, if this message was presented to a user it wouldn't cause too much of a problem.
On the other hand one of the most brutal ways to get information about an exception is to use the ToString method of the exception. Suppose that you modify the earlier example of IntegerDivide to change the displayed information about the exception, such as using ToString as follows:
Private Function IntegerDivide5(iItems As Integer, iTotal As Integer) As String Dim result As String ' Code that might throw an exception is wrapped in a Try block Try ' This will cause an exception to be thrown if iItems = 0 result = iTotal iItems Catch ex As Exception ' If the calculation failed, you get here result = ex.ToString End Try Return result End Function
The message shown in Figure 6.2 is helpful to a developer because it contains a lot of information, but it's not something you would typically want users to see. Instead, a user normally needs to see a short description of the problem. For the user, a message that looks like string in the Message property is appropriate. On the other hand, what the ToString method returns is, in fact, the concatenation of two properties: the message and the stack trace.
The Source and StackTrace properties provide information regarding where an error occurred. This supplemental information can be useful. In the case of the Source property, the text returns the name of the assembly where the error occurred. You can test this by replacing the ToString method in IntegerDivide5 with the Source property and rerunning the test.
The StackTrace is typically seen as more useful. This property not only returns information about the method where the error occurred, but returns information about the full set of methods used to reach the point where the error occurred. As noted, Figure 6.2 includes a sample of what is returned when you review this property.
The InnerException property is used to store an exception trail. This comes in handy when multiple exceptions occur. It's quite common for an exception to occur that sets up circumstances whereby further exceptions are raised. As exceptions occur in a sequence, you can choose to stack them for later reference by use of the InnerException property of your Exception object. As each exception joins the stack, the previous Exception object becomes the inner exception in the stack.
For simplicity, we're going to copy the current IntegerDivide5 method to create an inner exception handler in a new method IntegerDivide6. In this exception handler, when the divide by zero error occurs, create a new exception, passing the original exception as part of the constructor. Then in the outer exception handler, unwind your two exceptions displaying both messages.
The following code snippet illustrates the new method IntegerDivide6, and the results of running this new method are shown in Figure 6.3.
Private Function IntegerDivide6(iItems As Integer, iTotal As Integer) As String Dim result As String ' Code that might throw an exception is wrapped in a Try block Try Try ' This will cause an exception to be thrown if iItems = 0 result = iTotal iItems Catch ex As Exception Dim myException = New Exception( "IntegerDivide6: My Generic Exception", ex) Throw myException End Try Return result Catch ex As Exception ' If the calculation failed, you get here result = "Outer Exception: " & ex.Message & vbCrLf result += "Inner Exception: " & ex.InnerException.Message End Try Return result End Function
Figure 6.3 shows your custom message as the outer exception. Then the InnerException is referenced and the original error message, the divide-by-zero exception, is displayed.
The GetBaseException method comes in very handy when you are deep in a set of thrown exceptions. This method returns the originating exception by recursively examining the InnerException until it reaches an exception object that has a null InnerException property. That exception is normally the exception that started the chain of unanticipated events.
To illustrate this, modify the code in IntegerDivide6 and replace the original Outer and Inner exception messages with a single line of code as shown in the following code snippet:
'result = "Outer Exception: " & ex.Message & vbCrLf 'result += "Inner Exception: " & ex.InnerException.Message result = "Base Exception: " & ex.GetBaseException.Message
As shown in Figure 6.4 the code traverses back to the original exception and displays only that message.
The HelpLink property gets or sets the help link for a specific Exception object. It can be set to any string value, but it's typically set to a URL. If you create your own exception in code, you might want to set HelpLink to a URL (or a URN) describing the error in more detail. Then the code that catches the exception can go to that link. You could create and throw your own custom application exception with code like the following:
Dim ex As New ApplicationException("A short description of the problem") ex.HelpLink = "http://mysite.com/somehtmlfile.htm" Throw ex
When trapping an exception, the HelpLink can be used with something like Process.Start to start Internet Explorer using the help link to take the user directly to a help page, where they can see details about the problem.