Chapter 8. Exception Handling and Debugging

All professional-grade programs need to handle unexpected conditions. In programming languages before Microsoft .NET, this was often called error handling. Unexpected conditions generated error codes, which were trapped by programming logic that took appropriate action.

The common language runtime in .NET does not generate error codes. When an unexpected condition occurs, the CLR creates a special object called an exception. This object contains properties and methods that describe the unexpected condition in detail and provide various items of useful information about what went wrong.

Because the .NET Framework deals with exceptions instead of errors, the term error handling is seldom used in the .NET world. Instead, the term exception handling is preferred. This term refers to the techniques used in .NET to detect exceptions and take appropriate action.

This chapter covers how exception handling works in Visual Basic 2008. It discusses the common language runtime (CLR) exception handler in detail and the programming methods that are most efficient in catching errors. Specifically, it covers the following:

  • A very brief overview of error handling in Visual Basic 6 (VB6), for those just moving to .NET

  • The general principles behind exception handling

  • The Try...Catch...Finally structure, the Exit Try statement, and nested Try structures

  • The exception object's methods and properties

  • Capabilities in Visual Studio for working with exceptions

  • Error and trace logging and how you can use these methods to obtain feedback about how your program is working

A Brief Review of Error Handling in VB6

For compatibility, Visual Basic 2008 and other .NET versions of Visual Basic still support the old-style syntax for error handling that was used in Visual Basic 6 and earlier versions. That means you can still use the syntax presented in this review. However, it is strongly recommended that you avoid using this old-style syntax in favor of the exception handling features that are native to .NET. Using the more modern Try...Catch syntax (presented after this review) will give you more flexibility and better code structure.

The old-style syntax in VB6 was handed down from DOS versions of BASIC. The On Error construct was created in an era when line labels and GoTo statements were commonly used. Such error handling is difficult to use and has limited functionality compared to more modern alternatives.

In VB6, a typical routine with error handling code looks like this:

Private Function OpenFile(sFileName As String) As Boolean

On Error GoTo ErrHandler:
Open sFileName For Random As #1
OpenFile = True
Exit Sub

ErrHandler:
Select Case Err.Number
    Case 53 ' File not found
        MessageBox.Show "File not found"
    Case Else
         MessageBox.Show "Other error"
End Select
OpenFile = False

End Function

The top of the routine points to a section of code called an error handler, which is usually placed at the bottom of the routine. The error handler takes control as soon as an error is detected in the routine, checking the error number to determine what action to take. The error number is available as a property of the Err object, which is a globally available object that holds error information in VB6.

There are several other error-handling syntax options not included in the preceding error-handling code. If the error handler can take care of the error without breaking execution, then it can resume execution with the line of code that generated the error (Resume), the one after that (Resume Next), or at a particular location (Resume LineLabel).

Error-handling code becomes more complex if the error handling needs to vary in the routine. Multiple On Error GoTo statements must be used to send errors to various error handlers, all of which are clustered at the bottom of the routine. With such a lack of organization of error-handling code, it is easy to become confused about what should happen under various conditions. There is also very little information available about the error during the process, except for the error number. You can't determine, for example, the line number on which the error was generated without single-stepping through the code.

Such logic can rapidly become convoluted and unmanageable. There's a much better way to manage errors in VB 2008: structured exception handling. The rest of this chapter explains this technique for working with code errors, and uses the term structured exception handling throughout, except for the small sections that discuss compatibility with older error-handling techniques.

Exceptions in .NET

.NET implements a systemwide, comprehensive approach to exception handling. As noted in the chapter introduction, instead of an error number, .NET uses an exception object. This object contains information relevant to the error, exposed as properties of the object. Later you'll see a table that summarizes the properties and information they expose.

Such an object is an instance of a class that derives from a class named System.Exception. As shown later, a variety of subclasses of System.Exception are used for different circumstances.

Important Properties and Methods of an Exception

The Exception class has properties that contain useful information about the exception, as shown in the following table:

Property

Description

HelpLink

A string indicating the link to help for this exception

InnerException

Returns the exception object reference to an inner (nested) exception

Message

A string that contains a description of the error, suitable for displaying to users

Source

A string containing the name of an object that generated the error

StackTrace

A read-only property that holds the stack trace as a text string. The stack trace is a list of the pending method calls at the point at which the exception was detected. That is, if MethodA called MethodB, and an exception occurred in MethodB, the stack trace would contain both MethodA and MethodB.

TargetSite

A read-only string property that holds the method that threw the exception

The two most important methods of the Exception class are as follows:

Method

Description

GetBaseException

Returns the first exception in the chain

ToString

Returns the error string, which might include as much information as the error message, the inner exceptions, and the stack trace, depending on the error

You will see these properties and methods used in the code examples shown later, after you have covered the syntax for detecting and handling exceptions.

How Exceptions Differ from the Err Object in VB6

Because an exception contains all of the information needed about an error, structured exception handling does not use error numbers and the Err object. The exception object contains all the relevant information about the error.

However, whereas there is only one global Err object in VB6, there are many types of exception objects in the .NET Framework. For example, if a divide by zero is done in code, then an OverflowException is generated. In addition to the dozens of exception types available in the .NET Framework, you can inherit from a class called ApplicationException and create your own exception classes (see Chapter 3 for a discussion of inheritance).

In .NET, all exceptions inherit from System.Exception. Special-purpose exception classes can be found in many namespaces. The following table lists four representative examples of the classes that extend Exception:

Namespace

Class

Description

System

InvalidOperationException

Generated when a call to an object method is inappropriate because of the object's state

System

OutOfMemoryException

Results when there is not enough memory to carry out an operation

System.XML

XmlException

Often caused by an attempt to read invalid XML

System.Data

DataException

Represents errors in ADO.NET components

There are literally dozens of exception classes scattered throughout the .NET Framework namespaces. It is common for an exception class to reside in a namespace with the classes that typically generate the exception. For example, the DataException class is in System.Data, with the ADO.NET components that often generate a DataException instance.

Having many types of exceptions in VB 2008 enables different types of conditions to be trapped with different exception handlers. This is a major advance over VB6. The syntax to accomplish that is discussed next.

Structured Exception-Handling Keywords

Structured exception handling depends on several keywords in VB 2008:

  • Try — Begins a section of code in which an exception might be generated from a code error. This section of code is often called a Try block. A trapped exception is automatically routed to a Catch statement (discussed next).

  • Catch — Begins an exception handler for a type of exception. One or more Catch code blocks follow a Try block, with each Catch block catching a different type of exception. When an exception is encountered in the Try block, the first Catch block that matches that type of exception receives control.

  • Finally — Contains code that runs when the Try block finishes normally, or when a Catch block receives control and then finishes. That is, the code in the Finally block always runs, regardless of whether an exception was detected. Typically, the Finally block is used to close or dispose of any resources, such as database connections, that might have been left unresolved by the code that had a problem.

  • Throw — Generates an exception. It's often done in a Catch block when the exception should be kicked back to a calling routine, or in a routine that has itself detected an error such as a bad argument passed in. Another common place to throw an exception is after a test on the arguments passed to a method or property, if it is discovered that the argument is not appropriate, such as when a negative number is passed in for a count that must be positive.

The Try, Catch, and Finally Keywords

Here is an example showing some typical, simple structured exception-handling code in VB 2008. In this case, the most likely source of an error is the iItems argument. If it has a value of zero, then this would lead to dividing by zero, which would generate an exception.

First, create a Windows Application in Visual Basic 2008 and place a button on the default Form1 created in the project. In the button's click event, place the following two lines of code:

Dim sngAvg As Single
sngAvg = GetAverage(0, 100)

Then put the following function in the form's code:

Private Function GetAverage(iItems As Integer, iTotal As Integer) as Single
     ' Code that might throw an exception is wrapped in a Try block
    Try
        Dim sngAverage As Single

        ' This will cause an exception to be thrown if iItems = 0
        sngAverage = CSng(iTotal  iItems)

        ' This only executes if the line above generated no error
        MessageBox.Show("Calculation successful")
        Return sngAverage

    Catch excGeneric As Exception
        ' If the calculation failed, you get here
        MessageBox.Show("Calculation unsuccessful - exception caught")
        Return 0
    End Try
End Function

This code traps all the exceptions with a single generic exception type, and you don't have any Finally logic. Run the program and press the button. You will be able to follow the sequence better if you place a breakpoint at the top of the GetAverage function and step through the lines.

Here is a more complex example that traps the divide-by-zero exception explicitly. This second version of the GetAverage function (notice that the name is GetAverage2) also includes a Finally block:

Private Function GetAverage2(iItems As Integer, iTotal As Integer) as Single
     ' Code that might throw an exception is wrapped in a Try block
    Try
        Dim sngAverage As Single

        ' This will cause an exception to be thrown.
        sngAverage = CSng(iTotal  iItems)

        ' This only executes if the line above generated no error.
        MessageBox.Show("Calculation successful")
        Return sngAverage
     Catch excDivideByZero As DivideByZeroException
        ' You'll get here with an DivideByZeroException in the Try block
        MessageBox.Show("Calculation generated DivideByZero Exception")
        Return 0
    Catch excGeneric As Exception
        ' You'll get here when any exception is thrown and not caught in
        ' a previous Catch block.
        MessageBox.Show("Calculation failed - generic exception caught")
        Return 0
    Finally
        ' Code in the Finally block will always run.
         MessageBox.Show("You always get here, with or without an error")
    End Try

End Function

This code contains two Catch blocks for different types of exceptions. If an exception is generated, then .NET will go down the Catch blocks looking for a matching exception type. That means the Catch blocks should be arranged with specific types first and more generic types after.

Place the code for GetAverage2 in the form, and place another button on Form1. In the Click event for the second button, place the following code:

Dim sngAvg As Single
sngAvg = GetAverage2(0, 100)

Run the program again and press the second button. As before, it's easier to follow if you set a breakpoint early in the code and then step through the code line by line.

The Throw Keyword

Sometimes a Catch block is unable to handle an error. Some exceptions are so unexpected that they should be "sent back up the line" to the calling code, so that the problem can be promoted to code that can decide what to do with it. A Throw statement is used for that purpose.

A 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.

You can see the Throw statement in action by changing the earlier code for GetAverage2 to look like this:

Private Function GetAverage3(iItems As Integer, iTotal as Integer) as Single
     ' Code that might throw an exception is wrapped in a Try block
    Try
        Dim sngAverage As Single

        ' This will cause an exception to be thrown.
        sngAverage = CSng(iTotal  iItems)

        ' This only executes if the line above generated no error.
        MessageBox.Show("Calculation successful")
        Return sngAverage

    Catch excDivideByZero As DivideByZeroException
        ' You'll get here with an DivideByZeroException in the Try block.
        MessageBox.Show("Calculation generated DivideByZero Exception")
         Throw excDivideByZero
        MessageBox.Show("More logic after the throw - never executed")

    Catch excGeneric As Exception
        ' You'll get here when any exception is thrown and not caught in
        ' a previous Catch block.
        MessageBox.Show("Calculation failed - generic exception caught")
         Throw excGeneric
    Finally
        ' Code in the Finally block will always run, even if
        ' an exception was thrown in a Catch block.
        MessageBox.Show("You always get here, with or without an error")
    End Try
End Function

Here is some code to call GetAverage3. You can place this code in another button's click event to test it out:

Try
    Dim sngAvg As Single
    sngAvg = GetAverage3(0, 100)
Catch exc As Exception
    MessageBox.Show("Back in the click event after an error")
Finally
    MessageBox.Show("Finally block in click event")
End Try

Throwing a New Exception

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. To show how this is done, let's change the aforementioned example to throw your own exception instead of the one caught in the Catch block:

Private Function GetAverage4(iItems As Integer, iTotal as Integer) as Single

    If iItems = 0 Then
        Dim excOurOwnException As New _
             ArgumentException("Number of items cannot be zero")

         Throw excOurOwnException
    End If


 ' Code that might throw an exception is wrapped in a Try block.
    Try
        Dim sngAverage As Single

        ' This will cause an exception to be thrown.
        sngAverage = CSng(iTotal  iItems)

        ' This only executes if the line above generated no error.
        MessageBox.Show("Calculation successful")
        Return sngAverage

    Catch excDivideByZero As DivideByZeroException
        ' You'll get here with an DivideByZeroException in the Try block.
        MessageBox.Show("Calculation generated DivideByZero Exception")
        Throw excDivideByZero
        MessageBox.Show("More logic after the thrown - never executed")
    Catch excGeneric As Exception
        ' You'll get here when any exception is thrown and not caught in
        ' a previous Catch block.
        MessageBox.Show("Calculation failed - generic exception caught")
         Throw excGeneric
    Finally
        ' Code in the Finally block will always run, even if
        ' an exception was thrown in a Catch block.
        MessageBox.Show("You always get here, with or without an error")
End Try
End Function

This code can be called from a button with similar code for calling GetAverage3. Just change the name of the function called to GetAverage4.

This technique is particularly well suited to dealing with problems detected in property procedures. Property Set procedures often do checking 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

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 GetAverage5(iItems As Integer, iTotal as Integer) As Single
     ' Code that might throw an exception is wrapped in a Try block.
    Try
        Dim sngAverage As Single

        ' This will cause an exception to be thrown.
        sngAverage = CSng(iTotal  iItems)

        ' This only executes if the line above generated no error.
        MessageBox.Show("Calculation successful")
        Return sngAverage

    Catch excDivideByZero As DivideByZeroException
        ' You'll get here with an DivideByZeroException in the Try block.
         If iItems = 0 Then
             Return 0
             Exit Try
         Else
             MessageBox.Show("Error not caused by iItems")
         End If
        Throw excDivideByZero
        MessageBox.Show("More logic after the thrown - never executed")

    Catch excGeneric As Exception
        ' You'll get here when any exception is thrown and not caught in
        ' a previous Catch block.
        MessageBox.Show("Calculation failed - generic exception caught")
        Throw excGeneric
    Finally
        ' Code in the Finally block will always run, even if
        ' an exception was thrown in a Catch block.
        MessageBox.Show("You always get here, with or without an error")
    End Try
End Sub

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 intY was 0). The Exit Try goes immediately to the Finally block and completes the processing there:

If iItems = 0 Then
  Return 0
  Exit Try
Else
  MessageBox.Show("Error not caused by iItems")
End If

Now, if the overflow exception is caused by something other than division by zero, then you'll get a message box displaying "Error not caused by iItems."

Nested Try Structures

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. You can alter the example under the section "The Throw Keyword" to demonstrate the following code:

Private Function GetAverage6(iItems As Integer, iTotal as Integer) As Single

    ' Code that might throw an exception is wrapped in a Try block.
    Try
        Dim sngAverage As Single

         ' Do something for performance testing... .
         Try
             LogEvent("GetAverage")
         Catch exc As Exception
             MessageBox.Show("Logging function unavailable")
         End Try

        ' This will cause an exception to be thrown.
        sngAverage = CSng(iTotal  iItems)

        ' This only executes if the line above generated no error.
        MessageBox.Show("Calculation successful")
        Return sngAverage

    Catch excDivideByZero As DivideByZeroException
        ' You'll get here with an DivideByZeroException in the Try block.
        MessageBox.Show("Error not divide by 0")
        Throw excDivideByZero
        MessageBox.Show("More logic after the thrown - never executed")
     Catch excGeneric As Exception
        ' You'll get here when any exception is thrown and not caught in
        ' a previous Catch block.
        MessageBox.Show("Calculation failed - generic exception caught")
        Throw excGeneric
Finally
        ' Code in the Finally block will always run, even if
        ' an exception was thrown in a Catch block.
        MessageBox.Show("You always get here, with or without an error")
    End Try
End Function

In the preceding example, you are assuming that a function exists to log an event. This function would typically be in a common library, and might log the event in various ways. You will look at logging exceptions in detail later in the chapter, but a simple LogEvent function might look like this:

Public Sub LogEvent(ByVal sEvent As String)
     FileOpen(1, "logfile.txt", OpenMode.Append)
     Print(1, DateTime.Now & "-" & sEvent & vbCrLf)
     FileClose(1)

End Sub

In this case, you don't want a problem logging an event, such as a "disk full" error, to crash the routine. The code for the GetAverage function triggers a message box to indicate trouble with the logging function.

A Catch block can be empty. In that case, it has a similar effect as On Error Resume Next in VB6: the exception is ignored. However, execution does not pick up with the line after the line that generated the error, but instead picks up with either the Finally block or the line after the End Try if no Finally block exists.

Using Exception Properties

The previous examples have displayed hard-coded messages in message boxes, which is obviously not a good technique for production applications. Instead, a message box or log entry describing an exception should provide as much information as possible concerning the problem. To do this, various properties of the exception can be used.

The most brutal way to get information about an exception is to use the ToString method of the exception. Suppose that you modify the earlier example of GetAverage2 to change the displayed information about the exception like this:

Private Function GetAverage2(ByVal iItems As Integer, ByVal iTotal As Integer) _
        As Single
    ' Code that might throw an exception is wrapped in a Try block.
    Try
        Dim sngAverage As Single

        ' This will cause an exception to be thrown.
        sngAverage = CSng(iTotal  iItems)
        ' This only executes if the line above generated no error.
         MessageBox.Show("Calculation successful")
        Return sngAverage
Catch excDivideByZero As DivideByZeroException
        ' You'll get here with an DivideByZeroException in the Try block.
         MessageBox.Show(excDivideByZero.ToString)
        Throw excDivideByZero
        MessageBox.Show("More logic after the thrown - never executed")

    Catch excGeneric As Exception
        ' You'll get here when any exception is thrown and not caught in
        ' a previous Catch block.
        MessageBox.Show("Calculation failed - generic exception caught")
        Throw excGeneric
    Finally
        ' Code in the Finally block will always run, even if
        ' an exception was thrown in a Catch block.
        MessageBox.Show("You always get here, with or without an error")
    End Try
End Function

When the function is accessed with iItems = 0, a message box similar to the one in Figure 8-1 will be displayed.

Figure 8-1

Figure 8.1. Figure 8-1

The Message Property

The message shown in Figure 8-1 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, and that is supplied by the Message property.

If the previous code is changed so that the Message property is used instead of ToString, then the message box will provide something like what is shown in Figure 8-2.

Figure 8-2

Figure 8.2. Figure 8-2

The InnerException and TargetSite Properties

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, you'll start a new code sample, with just a subroutine that generates its own exception. You'll include code to add a reference to an InnerException object to the exception you are generating with the Throw method.

This example also includes a message box to show what's stored in the exception's TargetSite property. As shown in the results, TargetSite will contain the name of the routine generating the exception — in this case, HandlerExample. Here's the code:

Sub HandlerExample()
  Dim intX As Integer
  Dim intY As Integer
  Dim intZ As Integer
  intY = 0
  intX = 5
  ' First Required Error Statement.
  Try
    ' Cause a "Divide by Zero"
    intZ = CType((intX  intY), Integer)
  ' Catch the error.
  Catch objA As System.DivideByZeroException
     Try
       Throw (New Exception("0 as divisor", objA))
     Catch objB As Exception
       Dim sError As String
       sError = "My Message: " & objB.Message & vbCrLf & vbCrLf
       sError &= "Inner Exception Message: " & _
           objB.InnerException.Message & vbCrLf & vbCrLf
       sError &= "Method Error Occurred: " & objB.TargetSite.Name
       MessageBox.Show(sError)
     End Try
  Catch
    Messagebox.Show("Caught any other errors")
  Finally
    Messagebox.Show(Str(intZ))
  End Try
End Sub

As before, you catch the divide-by-zero error in the first Catch block, and the exception is stored in objA so that you can reference its properties later.

You throw a new exception with a more general message ("0 as divisor") that is easier to interpret, and you build up your stack by appending objA as the InnerException object using an overloaded constructor for the Exception object:

Throw (New Exception("0 as divisor", objA))

You catch your newly thrown exception in another Catch statement. Note how it does not catch a specific type of error:

Catch objB As Exception

Then you construct an error message for the new exception and display it in a message box:

Dim sError As String
sError = "My Message: " & objB.Message & vbCrLf & vbCrLf
sError &= "Inner Exception Message: " & _
    objB.InnerException.Message & vbCrLf & vbCrLf
sError &= "Method Error Occurred: " & objB.TargetSite.Name
MessageBox.Show(sError)

The message box that is produced is shown in Figure 8-3.

Figure 8-3

Figure 8.3. Figure 8-3

First your own message is included, based on the new exception thrown by your own code. Then the InnerException gets the next exception in the stack, which is the divide-by-zero exception, and its message is included. Finally, the TargetSite property gives you the name of the method that threw the exception. TargetSite is particularly helpful in logs or error reports from users that are used by developers to track down unexpected problems.

After this message box, the Finally clause displays another message box that just shows the current value of intZ, which is zero because the divide failed. This second box also occurs in other examples that follow.

Source and StackTrace

The Source and StackTrace properties provide the user with information regarding where the error occurred. This supplemental information can be invaluable, as the user can pass it on to the troubleshooter in order to help resolve errors more quickly. The following example uses these two properties and shows the feedback when the error occurs:

Sub HandlerExample2()

 Dim intX As Integer
 Dim intY As Integer
 Dim intZ As Integer
 intY = 0
 intX = 5
' First Required Error Statement.
  Try
    ' Cause a "Divide by Zero"
    intZ = CType((intX  intY), Integer)
  ' Catch the error.
  Catch objA As System.DivideByZeroException
     objA.Source = "HandlerExample2"
     Messagebox.Show("Error Occurred at :" & _
         objA.Source & objA.StackTrace)
  Finally
    Messagebox.Show(Str(intZ))
  End Try
End Sub

The output from the Messagebox statement is very detailed, providing the entire path and line number where the error occurred, as shown in Figure 8-4.

Figure 8-4

Figure 8.4. Figure 8-4

Notice that this information is also included in the ToString method examined earlier (refer to Figure 8-1).

GetBaseException

The GetBaseException method comes in very handy when you are deep in a set of thrown exceptions. This method returns the originating exception, which makes debugging easier and helps keep the troubleshooting process on track by sorting through information that can be misleading:

Sub HandlerExample3()
  Dim intX As Integer
  Dim intY As Integer
  Dim intZ As Integer
  intY = 0
  intX = 5
  ' First Required Error Statement.
  Try
    ' Cause a "Divide by Zero"
    intZ = CType((intX  intY), Integer)
  ' Catch the error.
  Catch objA As System.DivideByZeroException
Try
       Throw (New Exception("0 as divisor", objA))
     Catch objB As Exception
       Try
         Throw (New Exception("New error", objB))
       Catch objC As Exception
         Messagebox.Show(objC.GetBaseException.Message)
       End Try
     End Try
  Finally
    Messagebox.Show(Str(intZ))
  End Try
End Sub

The InnerException property provides the information that the GetBaseException method needs, so as your example executes the Throw statements, it sets up the InnerException property. The purpose of the GetBaseException method is to provide the properties of the initial exception in the chain that was produced. Hence, objC.GetBaseException.Message returns the Message property of the original OverflowException message even though you've thrown multiple errors since the original error occurred:

Messagebox.Show(objC.GetBaseException.Message)

To put it another way, the code traverses back to the exception caught as objA and displays the same message as the objA.Message property would, as shown in Figure 8-5.

Figure 8-5

Figure 8.5. Figure 8-5

HelpLink

The HelpLink property gets or sets the help link for a specific Exception object. It can be set to any string value, but is typically set to a URL. If you create your own exception in code, you might want to set HelpLink to some URL 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 exc As New ApplicationException("A short description of the problem")
exc.HelpLink = "http://mysite.com/somehtmlfile.htm"
Throw exc

When trapping an exception, the HelpLink can be used to launch a viewer so the user can see details about the problem. The following example shows this in action, using the built-in Explorer in Windows:

Sub HandlerExample4()
Try

    Dim exc As New ApplicationException("A short description of the problem")
    exc.HelpLink = "http://mysite.com/somehtmlfile.htm"
    Throw exc


    ' Catch the error.
Catch objA As System.Exception
    Shell("explorer.exe " & objA.HelpLink)


End Try
End Sub

This results in launching Internet Explorer to show the page specified by the URL. Most exceptions thrown by the CLR or the .NET Framework's classes have a blank HelpLink property. You should only count on using HelpLink if you have previously set it to a URL (or some other type of link information) yourself.

Interoperability with VB6-Style Error Handling

Because VB 2008 still supports the older On Error statement from pre-.NET versions of VB, you may encounter code that handles errors with On Error instead of with structured exception handling. You can use both techniques in a single program, but it is not possible to use both in a single routine. If you attempt to use both On Error and Try...Catch in a single routine, you will get a syntax error.

The VB compiler does allow the two techniques for handling errors to communicate with each other. For example, suppose you have a routine that uses On Error and then uses Err.Raise to promote the error to the calling code. Also suppose that the calling code makes the call in a Try...Catch block. In that case, the error created by Err.Raise becomes an exception in the calling code and is trapped by a Catch block just as a normal exception would be. Here's a code example to illustrate. First, create a subroutine that creates an error with Err.Raise, like this:

Private Sub RaiseErrorWithErrRaise()
     Err.Raise(53)   ' indicates File Not Found
End Sub

Then call this routine from a button's click event, with the call inside a Try...Catch block:

Private Sub Button2_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles Button2.Click
    Try
        RaiseErrorWithErrRaise()
    Catch ex As Exception
        MessageBox.Show(ex.Message)
    End Try
End Sub

When the button is clicked, it will display a message box with File Not Found. Even though the File Not Found error is raised by Err.Raise, it is translated to a .NET exception automatically.

Similarly, exceptions that are generated by a Throw statement in a called routine can be trapped by On Error in a calling routine. The exception is then translated into an Err object that works like the VB6 Err object.

Error Logging

Error logging is important in many applications for thorough troubleshooting. It is common for end users of an application to forget exactly what the error said. Recording specific errors in a log enables you to get the specific error message without recreating the error.

While error logging is very important, you only want to use it to trap specific levels of errors because it carries overhead and can reduce the performance of your application. Specifically, log only errors that are critical to your application integrity — for instance, an error that would cause the data that the application is working with to become invalid.

There are three main approaches to error logging:

  • Write error information in a text file or flat file located in a strategic location.

  • Write error information to a central database.

  • Write error information to the system Event Log, which is available on all versions of Windows supported by the .NET Framework 3.0. The .NET Framework includes a component that can be used to write to and read from the System, Application, and Security Logs on any given machine.

The type of logging you choose depends on the categories of errors you wish to trap and the types of machines on which you will run your application. If you choose to write to the Event Log, then you need to categorize the errors and write them in the appropriate log file. Resource-, hardware-, and system-level errors fit best into the System Event Log. Data access errors fit best into the Application Event Log. Permission errors fit best into the Security Event Log.

The Event Log

Three Event Logs are available: the System, Application, and Security Logs. Events in these logs can be viewed using the Event Viewer, which is accessed from the Control Panel. Access Administrative Tools and then select the Event Viewer subsection to view events. Typically, your applications would use the Application Event Log.

Event logging is available in your program through an EventLog component, which can both read and write to all of the available logs on a machine. The EventLog component is part of the System.Diagnostics namespace. This component allows adding and removing custom Event Logs, reading and writing to and from the standard Windows Event Logs, and creating customized Event Log entries.

Event Logs can become full, as they have a limited amount of space, so you only want to write critical information to your Event Logs. You can customize each of your system Event Log's properties by changing the log size and specifying how the system will handle events that occur when the log is full. You can configure the log to overwrite data when it is full or overwrite all events older than a given number of days. Remember that the Event Log that is written to is based on where the code is running from, so if there are many tiers, then you must locate the proper Event Log information to research the error further.

There are five types of Event Log entries you can make. These five types are divided into event type entries and audit type entries.

Event type entries are as follows:

  • Information — Added when events such as a service starting or stopping occurs

  • Warning — Occurs when a noncritical event happens that might cause future problems, such as disk space getting low

  • Error — Should be logged when something occurs that prevents normal processing, such as a startup service not being able to start

Audit type entries usually go into the Security Log and can be either of the following:

  • Success audit — For example, a success audit might be a successful login through an application to an SQL Server.

  • Failure audit — A failure audit might come in handy if a user doesn't have access to create an output file on a certain file system.

If you don't specify the type of Event Log entry, an information type entry is generated.

Each entry in an Event Log has a Source property. This required property is a programmer-defined string that is assigned to an event to help categorize the events in a log. A new Source must be defined prior to being used in an entry in an Event Log. The SourceExists method is used to determine whether a particular source already exists on the given computer. Use a string that is relevant to where the error originated, such as the component's name. Packaged software often uses the software name as the Source in the Application Log. This helps group errors that occur by specific software package.

The EventLog component is in the System.Diagnostics namespace. To use it conveniently, include an Imports System.Diagnostics statement in the declarations section of your code.

Note

Certain security rights must be obtained in order to manipulate Event Logs. Ordinary programs can read all of the Event Logs and write to the Application Event Log. Special privileges, on the administrator level, are required to perform tasks such as clearing and deleting Event Logs. Your application should not normally need to do these tasks, or write to any log besides the Application Event Log.

The most common events, methods, and properties for the EventLog component are listed and described in the following tables.

Events, Methods, and Properties

The following table describes the relevant event:

Event

Description

EntryWritten

Generated when an event is written to a log

The following table describes the relevant methods:

Methods

Description

CreateEventSource

Creates an event source in the specified log

DeleteEventSource

Deletes an event source and associated entries

WriteEntry

Writes a string to a specified log

Exists

Used to determine whether a specific Event Log exists

SourceExists

Used to determine whether a specific source exists in a log

GetEventLogs

Retrieves a list of all Event Logs on a particular computer

Delete

Deletes an entire Event Log. Use this method with care.

The following table describes the relevant properties:

Properties

Description

Source

Specifies the source of the entry to be written

Log

Used to specify a log to write to. The three logs are System, Application, and Security. The System Log is the default if not specified.

The following example illustrates some of these methods and properties:

Sub LoggingExample1()
  Dim objLog As New EventLog()
  Dim objLogEntryType As EventLogEntryType
  Try
    Throw (New EntryPointNotFoundException())
  Catch objA As System.EntryPointNotFoundException
    If Not EventLog.SourceExists("Example") Then
      EventLog.CreateEventSource("Example", "System")
    End If
    objLog.Source = "Example"
objLog.Log = "System"
    objLogEntryType = EventLogEntryType.Information
    objLog.WriteEntry("Error: " & objA.Message, objLogEntryType)
  End Try
End Sub

The preceding code declares two variables: one to instantiate your log and one to hold your entry's type information. Note that you need to check for the existence of a source prior to creating it. The following two lines of code accomplish that:

If Not EventLog.SourceExists("Example") Then
  EventLog.CreateEventSource("Example", "System")

After you have verified or created your source, you can set the Source property of the EventLog object, the Log property to specify which log you want to write to, and EventLogEntryType to Information (other options are Warning, Error, SuccessAudit, and FailureAudit). If you attempt to write to a source that does not exist in a specific log, then you get an error. After you have set these three properties of the EventLog object, you can then write your entry. In this example, you concatenated the word Error with the actual exception's Message property to form the string to write to the log:

objLog.Source = "Example"
objLog.Log = "System"
objLogEntryType = EventLogEntryType.Information
objLog.WriteEntry("Error: " & objA.Message, objLogEntryType)

Writing to Trace Files

As an alternative to the Event Log, you can write your debugging and error information to trace files. A trace file is a text-based file that you generate in your program to track detailed information about an error condition. Trace files are also a good way to supplement your event logging if you want to track detailed information that would potentially fill the Event Log.

A more detailed explanation of the variety of trace tools and their uses in debugging follows in the section "Analyzing Problems and Measuring Performance via the Trace Class." This section covers some of the techniques for using the StreamWriter interface in your development of a trace file.

The concepts involved in writing to text files include setting up streamwriters and debug listeners. The StreamWriter interface is handled through the System.IO namespace. It enables you to interface with the files in the file system on a given machine. The Debug class interfaces with these output objects through listener objects. The job of any listener object is to collect, store, and send the stored output to text files, logs, and the Output window. In the example, you will use the TextWriterTraceListener interface.

As you will see, the StreamWriter object opens an output path to a text file, and by binding the StreamWriter object to a listener object you can direct debug output to a text file.

Trace listeners are output targets and can be a TextWriter or an EventLog, or can send output to the default Output window (which is DefaultTraceListener). The TextWriterTraceListener accommodates the WriteLine method of a Debug interface by providing an output object that stores information to be flushed to the output stream, which you set up by the StreamWriter interface.

The following table lists some of the commonly used methods from the StreamWriter object:

Method

Description

Close

Closes the StreamWriter

Flush

Flushes all content of the StreamWriter to the output file designated upon creation of the StreamWriter

Write

Writes byte output to the stream. Optional parameters allow location designation in the stream (offset).

WriteLine

Writes characters followed by a line terminator to the current stream object

The following table lists some of the methods associated with the Debug object, which provides the output mechanism for the text file example to follow:

Method

Description

Assert

Checks a condition and displays a message if False

Close

Executes a flush on the output buffer and closes all listeners

Fail

Emits an error message in the form of an Abort/Retry/Ignore message box

Flush

Flushes the output buffer and writes it to the listeners

Write

Writes bytes to the output buffer

WriteLine

Writes characters followed by a line terminator to the output buffer

WriteIf

Writes bytes to the output buffer if a specific condition is True

WriteLineIf

Writes characters followed by a line terminator to the output buffer if a specific condition is True

The following example shows how you can open an existing file (called mytext.txt) for output and assign it to the Listeners object of the Debug object so that it can catch your Debug.WriteLine statements:

Sub LoggingExample2()
  Dim objWriter As New _
      IO.StreamWriter("C:mytext.txt", True)
   Debug.Listeners.Add(New TextWriterTraceListener(objWriter))
  Try
    Throw (New EntryPointNotFoundException())
  Catch objA As System.EntryPointNotFoundException
    Debug.WriteLine(objA.Message)
objWriter.Flush()
    objWriter.Close()
    objWriter = Nothing
  End Try
End Sub

Looking in detail at this code, you first create a StreamWriter that is assigned to a file in your local file system:

Dim objWriter As New _
    IO.StreamWriter("C:mytext.txt", True)

You then assign your StreamWriter to a debug listener by using the Add method:

Debug.Listeners.Add(New TextWriterTraceListener (objWriter))

This example forces an exception and catches it, writing the Message property of the Exception object (which is Entry point was not found) to the debug buffer through the WriteLine method:

Debug.WriteLine(objA.Message)

Finally, you flush the listener buffer to the output file and free your resources:

objWriter.Flush()
objWriter.Close()
objWriter = Nothing

Analyzing Problems and Measuring Performance via the Trace Class

The trace tools in the .NET Framework make use of the Trace class, which provides properties and methods that help you trace the execution of your code. By default, tracing is enabled in VB 2008, so not unlike the previous debug discussion, all you have to do is set up the output and utilize its capabilities.

You can specify the detail level you want to perform for your tracing output by configuring trace switches. You will see an example of setting a trace switch shortly, but it is important to understand what a trace switch can do and what the settings for trace switches mean.

Trace switches can be either BooleanSwitch or TraceSwitch. BooleanSwitch has a value of either 0 or 1 and is used to determine whether tracing is off or on, respectively, whereas TraceSwitch enables you to specify a level of tracing based on five enumerated values. You can manage a BooleanSwitch or TraceSwitch as an environment variable. Once a switch is established, you can create and initialize it in code and use it with either trace or debug.

A TraceSwitch can have five enumerated levels, which can be read as 04 or checked with four properties provided in the switch class interface. The four properties return a Boolean value based on whether the switch is set to a certain level or higher. The five enumerated levels for TraceSwitch are as follows:

Level

Description

0

None

1

Only error messages

2

Warning and error messages

3

Information, warning, and error messages

4

Verbose, information, warning, and error messages

The four properties are TraceError, TraceWarning, TraceInfo, and TraceVerbose. For example, if your switch were set at number 2 and you asked for the TraceError or TraceWarning properties, they would return True, whereas the TraceInformation and TraceVerbose properties would return False.

An environment variable is managed either via the command line or under My computer

Analyzing Problems and Measuring Performance via the Trace Class

From the command line, type Set _Switch_MySwitch = 0

The value on the left of the = symbol is the name of the switch; the value on its right is either 0 or 1 for a BooleanSwitch or 04 for a TraceSwitch. Note the space between the word Set and the leading underscore of _Switch. Once you have typed this line, if you follow that by the plain SET command at the command line, it will show your new switch as an environment variable, as shown in Figure 8-6.

Figure 8-6

Figure 8.6. Figure 8-6

For the example that follows, the output is directed to the default Output window:

Sub TraceExample1()
  Dim objTraceSwitch As TraceSwitch
  objTraceSwitch = New TraceSwitch("ExampleSwitch", "Test Trace Switch")
  objTraceSwitch.Level = TraceLevel.Error
  Try
    Throw (New EntryPointNotFoundException())
  Catch objA As System.EntryPointNotFoundException
    Trace.WriteLineIf(objTraceSwitch.TraceVerbose, _
        "First Trace " & objA.Source)
    Trace.WriteLineIf(objTraceSwitch.TraceError, _
        "Second Trace " & objA.Message)
  End Try
End Sub

You begin by assigning your switch to an existing registry entry and setting its level:

objTraceSwitch = New TraceSwitch("ExampleSwitch", "Test Trace Switch")
objTraceSwitch.Level = TraceLevel.Error

After you throw your exception, you first cause your trace output listener to catch the Source property of your Exception object based on whether the value of your switch is TraceVerbose or better:

Trace.WriteLineIf(objTraceSwitch.TraceVerbose, _
    "First Trace " & objA.Source)

Because the tracing level is set to Error, this line is skipped; and you continue by writing a trace to the Output window to include the message information if the level is set to Error:

Trace.WriteLineIf(objTraceSwitch.TraceError, _
    "Second Trace " & objA.Message)

As indicated in your Output window, you successfully wrote only the second trace line, based on the level being Error on your trace switch (see Figure 8-7).

Figure 8-7

Figure 8.7. Figure 8-7

Tracing can also be helpful in determining the performance of your application. Overall, your application might appear to be working fine, but it is always good to be able to measure your application's performance so that environment changes or degradation over time can be counteracted. The basic concept here is to use conditional compilation so that you can toggle your performance-measuring code on and off:

Sub TraceExample2()
  Dim connInfo As New Connection()
  Dim rstInfo As New Recordset()
  #Const bTrace = 1
  Dim objWriter As New _
    IO.StreamWriter(IO.File.Open("c:mytext.txt", IO.FileMode.OpenOrCreate))
  connInfo.ConnectionString = "Provider = sqloledb.1" & _
    ";Persist Security Info = False;" & "Initial Catalog = Northwind;" & _
    "DataSource = LocalServer"
  connInfo.Open(connInfo.ConnectionString, "sa")
  Trace.Listeners.Add(New TextWriterTraceListener(objWriter))
  #If bTrace Then
    Trace.WriteLine("Begun db query at " & now())
  #End If
  rstInfo.Open("SELECT CompanyName, OrderID, " & _
    "OrderDate FROM Orders AS a LEFT JOIN Customers" & _
    " AS b ON a.CustomerID = b.CustomerID WHERE " & _
    "a.CustomerID = 'Chops'", connInfo, _
    CursorTypeEnum.adOpenForwardOnly, _
    LockTypeEnum.adLockBatchOptimistic)
  #If bTrace Then
    Trace.WriteLine("Ended db query at " & now())
  #End If
  Trace.Listeners.Clear()
  objWriter.Close()
  rstInfo.Close()
  connInfo.Close()
  rstInfo = Nothing
  connInfo = Nothing
End Sub

Note

This subroutine uses ADO, so be sure to add a reference to an ADO library and an Imports ADODB statement in the declarations section of the module.

In this simple example, you are trying to measure the performance of a database query using a conditional constant defined as bTrace by the following code:

#Const bTrace = 1

You establish your database connection strings, and then right before you execute your query you write to a log file based on whether you are in tracing mode or not:

#If bTrace Then
  Trace.WriteLine("Begun db query at " & now())
#End If

Again, after your query returns you write to your log only if you are in tracing mode:

#If bTrace Then
  Trace.WriteLine("Ended db query at" & now())
#End If

Always remember that tracing can potentially slow the application down, so use this functionality only when troubleshooting, not all the time.

Summary

This chapter reviewed the exception object and the syntax available to work with exceptions. You have looked at the various properties of exceptions and learned how to use the exposed information. You have also seen how to promote exceptions to consuming code using the Throw statement, and how structured exception handling interoperates with the old-style On Error. As discussed, any new code you write should use structured exception handling. Avoid using the old-style On Error except for maintenance tasks in old code.

Also covered were other topics related to error handling:

  • Error logging to Event Logs and trace files

  • Instrumentation and measuring performance

  • Tracing techniques

By using the full capabilities for error handling that are now available in VB 2008, you can make your applications more reliable and diagnose problems faster when they do occur. Proper use of tracing and instrumentation can also help you tune your application for better performance.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset