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
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.
.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.
The Exception
class has properties that contain useful information about the exception, as shown in the following table:
Property | Description |
---|---|
A string indicating the link to help for this exception | |
Returns the | |
A string that contains a description of the error, suitable for displaying to users | |
A string containing the name of an object that generated the error | |
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 | |
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 |
---|---|
Returns the first exception in the chain | |
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.
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
:
Class | Description | |
---|---|---|
| Generated when a call to an object method is inappropriate because of the object's state | |
| Results when there is not enough memory to carry out an operation | |
| Often caused by an attempt to read invalid XML | |
| 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 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.
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.
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
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 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
."
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.
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.
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.
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.
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.
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.
Notice that this information is also included in the ToString
method examined earlier (refer to Figure 8-1).
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.
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.
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 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.
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:
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.
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.
The following table describes the relevant event:
The following table describes the relevant methods:
Methods | Description |
---|---|
Creates an event source in the specified log | |
Deletes an event source and associated entries | |
Writes a string to a specified log | |
Used to determine whether a specific Event Log exists | |
Used to determine whether a specific source exists in a log | |
Retrieves a list of all Event Logs on a particular computer | |
| Deletes an entire Event Log. Use this method with care. |
The following table describes the relevant properties:
Properties | Description |
---|---|
| Specifies the source of the entry to be written |
| 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)
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 |
---|---|
Closes the | |
Flushes all content of the | |
| Writes byte output to the stream. Optional parameters allow location designation in the stream (offset). |
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 |
---|---|
Checks a condition and displays a message if | |
| Executes a flush on the output buffer and closes all listeners |
Emits an error message in the form of an Abort/Retry/Ignore message box | |
| Flushes the output buffer and writes it to the listeners |
| Writes bytes to the output buffer |
| Writes characters followed by a line terminator to the output buffer |
Writes bytes to the output buffer if a specific condition is | |
Writes characters followed by a line terminator to the output buffer if a specific condition is |
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
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 0
–4
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 |
---|---|
| None |
| Only error messages |
| Warning and error messages |
| Information, warning, and error messages |
| 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
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 0
–4
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.
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).
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
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.
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.