Visual Basic has included error handling since its initial release
through the On Error
statement.
Although often derided by developers, this mechanism did effectively
catch and process all errors when used properly. Visual Basic 2005 still
includes this error-handling methodology, but it also includes
structured error handling, new with .NET. This
chapter considers this new error-processing system, comprised of the
Try…Catch…Finally
statement and
System.Exception
-derived error
objects.
Although you’ve been a Visual Basic 6.0 developer for years, and
you’ve already used On Error
statements in your Visual Basic 2005 code, you want to try out the
structured error-handling statements you’ve heard so much
about.
Use the Try…Catch…Finally
block statement to locally monitor and handle errors.
The statement has three sections:
Try
The code you need to monitor for errors appears in this first section.
Catch
When an error occurs, processing jumps immediately from
the Try
section to a matching
Catch
block (We’ll define
“matching” shortly). Any remaining unprocessed statements in the
Try
block are ignored. You
can have any number of Catch
entries in your error-handling block.
Finally
Any code you include in this optional section runs whether
an exception occurs or not. It’s a useful place to put any
cleanup code related to resources you allocated in the Try
section.
Here’s the syntax of the Try…Catch…Finally
statement:
Try ' ----- Error-prone code here. Catch ex As System.Exception ' ----- Error-processing code here. Multiple ' Catch blocks can be included. Finally ' ----- Cleanup code here (optional). End Try
Although Visual Basic 2005 still supports the On Error
statement and related
error-handling logic found way back in Visual Basic 1.0, it also
includes a new “structured” error-handling system that more closely
parallels the object-oriented nature of .NET. In this system,
exceptions (errors) exist as objects, inherited from
the System.Exception
class. When an error occurs
in your code, .NET wraps it up in a System.Exception
object (or one of its more
specific derived classes) and triggers it in your code. The Try…Catch
statement watches for any such
exceptions and jumps to a Catch
block when an exception occurs.
System.Exception
represents
the most general type of exception; because all exception objects
derive from it, it catches all error types. In this statement:
Try ' ----- Error-prone code here. Catch ex As System.Exception ' ----- Error-processing code here. End Try
any type of error that occurs in the Try
block, no matter what it is, falls into
the Catch
block, since that block
catches every type of error.
.NET also defines more specific exceptions. For example, the
System.OutOfMemoryException
error
occurs when any operation lacks sufficient memory to execute
properly:
Try ' ----- Error-prone code here. Catch ex As System.OutOfMemoryException ' ----- Handle memory errors here. Catch ex As System.Exception ' ----- Handle all other errors here. End Try
Each Catch
block handles only
the error types specified in its As
clause. In the above block of code, the first Catch
block handles OutOfMemoryException
errors. Any other error
that occurs in the Try
block skips
over that first Catch
entry and
jumps into the second, more general Catch
block. This is what is meant by a
“matching” Catch
block, as
mentioned earlier in this recipe. Exceptions seek the first matching Catch
clause, based on an exact class match
or a derived match relationship.
When an error occurs, the generated exception is compared to
each Catch
block’s As
clause for a match, in order from top to
bottom. Therefore, you should place the most restrictive error type
first, saving System.Exception
for
the last Catch
block. If no error
occurs, all Catch
blocks are
ignored.
Within a Catch
block, the
ex
variable (included just after
the Catch
keyword) provides access
to the actual exception object. Use its members as you would the
members of any other object. A description of the exception appears as
ex.Message
. You can name the
variable anything you want; the name ex
has become common in technical
documentation, but you are free to change it or even vary it between
the different Catch
clauses.
If included, the Finally
block is always processed, no matter what. It is processed after the
relevant Try
and Catch
blocks complete. Even if you issue an
Exit Sub
or similar statement from
within a Try
or Catch
block, the Finally
section is still processed. All
Try
statements must include at
least one Catch
or Finally
block.
There are some restrictions on Try…Catch
statements. In general, you cannot
use GoTo
statements to jump into or
out of any of the blocks. There is an Exit
Try
statement that lets you jump out early, but it can’t be
used in the Finally
block.
If an error occurs in a routine but no error handling is in
effect (i.e., the code is out-side of a Try
statement, and no On Error
statements appear in the
procedure), the error “bubbles up” to the calling procedure, looking
for another active error handler to deal with the exception. If no
error handlers are available to deal with the error, a message is
displayed to the user, and the application exits.
Recipe 15.3 discusses a global exception handler that Catches any exceptions not dealt with in local procedures.
An invalid condition has occurred in your custom class code, and you want to generate an exception to inform the calling code of the problem.
Use the Throw
statement to send an exception to the
next available error handler. Throw
takes an instance of a System.Exception
(or derived) object as its
only argument:
Throw New System.Exception("A great big error occurred.")
You can also prepare your exception object in advance and then
use its variable in the Throw
statement:
Dim errorDetail As New System.ArgumentOutOfRangeException( _ "Year", "The 'Year' must be at least 1995.") Throw errorDetail
When .NET detects an error in your program, it also uses the
Throw
statement to send errors to
your code. When you use the Throw
statement, your generated errors look just like those issued by the
Framework.
You can generate an error at any time using the Throw
statement, even within a Try
block. The related Catch
handler will process the error as if
some other system-defined process had generated the error.
Visual Basic also includes an
Err.Raise
method that generates errors, as
was done using pre-.NET versions of Visual Basic. It focuses on error
numbers rather than on object-based exceptions. Although .NET will wrap errors issued
through Err.Raise
in an Exception
object, you should use this method
only for backward compatibility. Use the Throw
statement instead.
Although you make judicious use of Try…Catch
and On
Error
statements in your code, it’s possible that some
exceptions will sneak through your structured and unstructured
error-handling barriers. You want to keep these errors from crashing
the program.
Sample code folder: Chapter 15 UnhandledException
Handle the application-level UnhandledException
event to capture any
errors not dealt with elsewhere in your code. This global error
handler is part of the Windows Forms Application Framework. In the
Project Properties window’s Application panel, make sure that “Enable
application framework” is selected, and then click on the View
Application Events button on that same panel. Visual Studio opens the
ApplicationEvents.vb source file, which looks
like this:
Namespace My Partial Friend Class MyApplication End Class End Namespace
The global error handler will appear in this MyApplication
class. Select “(MyApplication
Events)” from the Class Name list above and to the left of the code
editor window, and then select “UnhandledException” from the Method
Name list just to the right of that. Visual Studio will add a template
for the UnhandledException
event
handler:
Private Sub MyApplication_UnhandledException( _ ByVal sender As Object, ByVal e As Microsoft. _ VisualBasic.ApplicationServices. _ UnhandledExceptionEventArgs) _ Handles Me.UnhandledException End Sub
Code added to this event handler will run whenever an unhandled
error or exception occurs somewhere in your application. Once you have
dealt with the error, you can either exit the application immediately
(in a more controlled manner than just letting the program crash) or
return to a basic waiting-for-input-from-the-user state. Use the
e
argument’s ExitApplication
property to indicate which
choice you want to make. Setting this property to True
, as shown here, will terminate the
program:
Private Sub MyApplication_UnhandledException( _ ByVal sender As Object, ByVal e As Microsoft. _ VisualBasic.ApplicationServices. _ UnhandledExceptionEventArgs) _ Handles Me.UnhandledException MsgBox("An unhandled error occurred. That's bad.") e.ExitApplication = True End Sub
This code is never called when your application runs in the debugger.
The solution listed above is valid only for Windows Forms applications that use the Application Framework. If you choose to disable the Application Framework, or you are writing a non–Windows Forms application, you must manually establish a global error handler for each thread of your application. We’ll look at the first case here.
Create a new Windows Forms application, and clear the “Enable
application frame-work” field in the Project Properties window. Open
up the source code window for the Form1
form, and replace the basically empty
content with the following code:
Public Class Form1 Private Sub Form1_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Me.Click ' ----- Cause a fake unhandled error. Throw New System.Exception( ) End Sub Private Sub Form1_FormClosed(ByVal sender As Object, _ ByVal e As System.Windows.Forms.FormClosedEventArgs) _ Handles Me.FormClosed ' ----- Disable the monitor before exiting. RemoveGlobalErrorMonitor( ) End Sub Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load ' ----- Enable error monitoring. AddGlobalErrorMonitor( ) End Sub End Class Module Module1 Public Sub AddGlobalErrorMonitor( ) ' ----- Enable global error monitoring on this thread. AddHandler Application.ThreadException, _ AddressOf GlobalErrorMonitor End Sub Public Sub RemoveGlobalErrorMonitor( ) ' ----- Disable global error monitoring on this thread. RemoveHandler Application.ThreadException, _ AddressOf GlobalErrorMonitor End Sub Public Sub GlobalErrorMonitor(ByVal sender As Object, _ ByVal e As System.Threading.ThreadExceptionEventArgs) ' ----- An unhandled global error occurred in the thread. MsgBox("A global error was caught.") End Sub End Module
This code uses the AddHandler
statement to connect the thread’s Application. ThreadException
event to a
custom event handler, GlobalErrorMonitor()
. It’s added immediately
when the (main) form is first loaded, and it remains until the form
closes. Remember that this code will not work properly within Visual
Studio. You must build the application and run it directly before your
global exception handler can be used.
When writing console applications, monitor the System.appdomain.CurrentDomain.
UnhandledException
event instead of Application.ThreadException
:
AddHandler System.appdomain.CurrentDomain. _ UnhandledException, AddressOf GlobalErrorMonitor
An error has occurred, and you want to inform the user in a friendly manner.
The captured exception object includes all the details
concerning the error, with some parts ready for user-friendly
presentation. The simplest presentation option uses the exception’s
ToString()
method to generate
information about the error.
The following code generates the error message in Figure 15-1 when run within Visual Studio:
Try Throw New System.Exception( ) Catch ex As System.Exception MsgBox(ex.ToString( )) End Try
If you encounter an exception in a block of code where you know errors are likely, you can sometimes compensate for the error through alternate logic without ever informing the user of the problem. In those cases where you cannot continue normally because of the error, your program can inform the user of the situation.
Beyond the basic ToString()
output, you can handcraft the details of the exception into a form
that better communicates the problem to the user. The System.Exception
object includes the
following useful properties:
Data
Some errors use the collection exposed by this property to store additional details related to the error. The type of data stored depends on the code that generated the error. It is most often used in custom exceptions.
InnerException
If this exception is a byproduct of another, earlier exception, this property exposes that previous exception.
Message
This property provides a short yet friendly description of the exception.
Source
This property specifies the name of the application, class, or process ID that generated the error.
StackTrace
This text property provides a semihuman-readable listing of the stack trace—the set of called methods that led up to the method generating the error. This stack trace may include internal procedures from the .NET Framework, and its overall length may shock the user.
TargetSite
This property exposes a MethodBase
object that fully describes
the procedure in which the exception occurred. The properties of
this object may or may not be useful in every case, especially
when an application has been obfuscated.
Other exception objects further derived from System.Exception
may include additional
properties with more detailed information. By concatenating the
various properties of the captured exception object, you should be
able to effectively communicate the problem to the user or store the
details in an error log for later analysis.
None of the exception objects supplied with .NET really meets the needs of the error you need to generate.
Build your own exception object by deriving a new class from
System.Exception
or another class
already derived from it.
The following class extends the standard Exception
object by adding a place for a SQL
statement used in a database query:
Public Class ExceptionWithSQL Inherits System.Exception Public SQLStatement As String Public Sub New(ByVal message As String, _ ByVal sqlText As String, _ ByVal innerException As System.Exception) ' ----- Store the details of this exception. MyBase.New(message, innerException) SQLStatement = sqlText End Sub End Class
Many business applications that interact with a database use a
central procedure to process SQL statements in a consistent manner.
While this procedure may have its own error handler, the calling code
also wants to know when an error occurred with the SQL statement that
it provided. The following ProcessSQL
method represents just such a
common procedure. If an error occurs in the supplied SQL statement, it
uses the ExceptionWithSQL
class to
communicate the problem:
Public Sub ProcessSQL(ByVal sqlText As String) Try ' ----- Add ADO.NET-specific code here. Catch ex As System.Exception ' ----- Convert this to a SQL error. Throw New WindowsApplication1.ExceptionWithSQL( _ "A SQL error occurred.", sqlText, ex) ' ----- The calling procedure will receive the ' modified error. End Try End Sub
Since the calling code may issue several different SQL
statements within a common Try
block, having the errant SQL statement in the exception object
provides the additional information a programmer may need to locate
the problem:
Dim sqlText As String Try sqlText = "DELETE FROM Table1 WHERE RecordType = 5" ProcessSQL(sqlText) sqlText = "DELETE FROM Table2 WHERE RecordType = 5" ProcessSQL(sqlText) Catch ex As WindowsApplication1.ExceptionWithSQL MsgBox("The following SQL statement caused an error:" & _ vbCrLf & ex.SQLStatement) End Try
You can also create a new ExceptionWithSQL
object for any reason on
your own and Throw
it, even if no
underlying database error occurred. With custom errors, the choice of
when to use them is yours.
Before .NET, errors in Visual Basic were identified solely by a number, many defined for common use by Microsoft Windows. For instance, error number 7 represents the “Out-of-memory” error condition.
In .NET, all errors are defined by specific classes derived from
System.Exception
. For example,
out-of-memory errors are thrown as instances of System.OutOfMemoryException
. You can derive
your own exceptions for use in your application code. You will
often derive such custom errors directly from System.Exception
, but if another derived
exception class contains features you don’t want to rewrite from
scratch, you can derive from that class instead.
The various .NET exceptions derived from System.Exception
can also be used directly.
For instance, you can throw a System.DivideByZeroException
even if you
don’t actually perform an invalid division, but your code has a
zero-value denominator ready to use:
Public Function CheckAndDivide(ByVal numerator As Decimal, _ ByVal denominator As Decimal) As Decimal ' ----- Divide numbers, but check for divide-by-zero first. If (denominator = 0@) Then Throw New System.DivideByZeroException( ) Else Return numerator / denominator End If End Function
You have a block of code that might generate errors, but you don’t really care. You want the code to continue on with or without errors and to provide no error report to the user.
To ignore errors, use the On Error Resume Next
statement, or use a
Try
statement with an empty
Catch
block.
In Visual Basic, the traditional way to ignore errors in a
section of code is to use the On Error Resume
Next
statement. The following code shows both ignored and
pro-cessed error-handler sections:
Public Sub DoSomething( ) On Error Resume Next ' ----- Error handling is now disabled. You can do ' dangerous things and no errors will occur. The ' "Err" object will still be filled in with ' error content when an error does occur, so you ' can check that if you are concerned. On Error GoTo ErrorHandler ' ----- Error handling has been turned back on. All ' errors will jump down to the labeled section. Exit Sub ErrorHandler: ' ----- Do something with the error here, then… Resume Next End Sub
If you want to ignore errors but prefer using the structured
exception-handling features, add a Try
block with an empty Catch
block:
Public Sub DoSomething( ) Try ' ----- As expected, any error that occurs here will ' jump to the Catch block. Catch ' ----- If you don't include any error-handling code ' here, the error is just ignored. End Try ' ----- Errors that occur out here will not be caught by ' the Try block, but you knew that already. End Sub
There is a small difference between these two blocks of code.
When using the On Error Resume Next
statement, any error on a statement causes the code to continue with
the next statement. In the Try…Catch
example, any error that occurs in
the Try
block causes the code to
continue with the Catch
block, and
then with the code that follows the entire Try…End Try
section. This means that if you
have multiple statements in the Try
block and an error occurs on the first of those statements, the
remaining statements in the Try
block are skipped completely.
3.145.86.211