In this chapter, we’ll cover two major Visual Basic programming topics: lambda expressions and error handling. Both are mysterious, one because it uses a Greek letter in its name and the other because it might as well be in Greek for all the difficulty programmers have with it. Lambda expressions in particular have to do with the broader concept of functional programming, the idea that every computing task can be expressed as a function and that functions can be passed around willy-nilly within the source code. Visual Basic is not a true functional programming language, but the introduction of lambda expressions in Visual Basic 2008 brings some of those functional ways and means to the language.
Lambda expressions are named for lambda calculus (or λ-calculus), a mathematical system designed in the 1930s by Alonzo Church, certainly a household name between the wars. Although his work was highly theoretical, it led to features and structures that benefit most programming languages today. Specifically, lambda calculus provides the rationale for the Visual Basic functions, arguments, and return values that we’ve already learned about. So, why add a new feature to Visual Basic and call it “lambda” when there are lambda things already in the language? Great question. No answer.
Lambda expressions let you define an object that contains an entire function. Although this is something new in Visual Basic, a similar feature has existed in the BASIC language for a long time. I found an old manual from the very first programming language I used, BASIC PLUS on the RSTS/E timeshare computer. It provided a sample of the DEF
statement, which let you define simple functions. Here is some sample code from that language that prints a list of the first five squares:
100 DEF SQR(X)=X*X 110 FOR I=1 TO 5 120 PRINT I, SQR(I) 130 NEXT I 140 END
The function definition for SQR( )
appears on line 100, returning the square of any argument passed to it. It’s used in the second half of line 120, generating the following output:
1 1 2 4 3 9 4 16 5 25
Lambda expressions in Visual Basic work in a similar way, letting you define a variable as a simple function. Here’s the Visual Basic equivalent for the preceding code:
Dim sqr As Func(Of Integer, Integer) = _ Function(x As Integer) x * x For i As Integer = 1 To 5 Console.WriteLine("{0}{1}{2}", i, vbTab, sqr(i)) Next i
The actual lambda expression is on the second line:
Function(x As Integer) x * x
Lambda expressions begin with the Function
keyword, followed by a list of passed-in arguments in parentheses. After that comes the definition of the function itself, an expression that uses the passed-in arguments to generate some final result. In this case, the result is the value of x multiplied by itself.
One thing you won’t see in a lambda expression is the Return
statement. Instead, the return value just seems to fall out of the expression naturally. That’s why you need some sort of variable to hold the definition and return the result in a function-like syntax.
Dim sqr As Func(Of Integer, Integer)
Lambda expression variables are defined using the Func
keyword—so original. The data type argument list matches the argument list of the actual lambda expression, but with an extra data type thrown in at the end that represents the return value’s data type. Here’s a lambda expression that checks whether an Integer
argument is even or not, returning a Boolean result:
Public Sub TestNumber( ) Dim IsEven As Func(Of Integer, Boolean) = _ Function(x As Integer) (x Mod 2) = 0 MsgBox("Is 5 Even? " & IsEven(5)) End Sub
This code displays a message that says, “Is 5 Even? False.” Behind the scenes, Visual Basic is generating an actual function, and linking it up to the variable using a delegate. (A delegate, as you probably remember, is a way to identify a method generically through a distinct variable.) The following code is along the lines of what the compiler is actually generating for the previous code sample:
Private Function HiddenFunction1( _ ByVal x As Integer) As Boolean Return (x Mod 2) = 0 End Function Private Delegate Function HiddenDelegate1( _ ByVal x As Integer) As Boolean Public Sub TestNumber( ) Dim IsEven As HiddenDelegate1 = _ AddressOf HiddenFunction1 MsgBox("Is 5 Even? " & IsEven(5)) End Sub
In this code, the lambda expression and related IsEven
variable have been replaced with a true function (HiddenFunction1
) and a go-between delegate (HiddenDelegate1
). Although lambdas are new in Visual Basic 2008, this type of equivalent functionality has been available since the first release of Visual Basic for .NET. Lambda expressions provide a simpler syntax when the delegate-referenced function is just returning a result from an expression.
Lambda expressions were added to Visual Basic 2008 primarily to support the new LINQ functionality (see Chapter 17). They are especially useful when you need to supply an expression as a processing rule for some other code, especially code written by a third party. And in your own applications, Microsoft is a third party. Coincidence? I think not!
Lambda expressions are good and all, but it’s clear that equivalent functionality was already available in the language. And by themselves, lambda expressions are just a simplification of some messy function-delegate syntax. But when you combine lambda expressions with the type inference features discussed back in Chapter 6, you get something even better: pizza!
Perhaps I should have written this chapter after lunch. What you get is lambda expressions with inferred types. It’s not a very romantic name, but it is a great new tool.
Let’s say that you wanted to write a lambda expression that multiplies two numbers together.
Dim mult As Func(Of Integer, Integer, Integer) = _ Function(x As Integer, y As Integer) x * y MsgBox(mult(5, 6)) ' Displays 30
This is the Big Cheese version of the code: I tell Visual Basic everything, and it obeys me without wavering. But there’s also a more laissez faire version of the code that brings type inference into play.
Dim mult = Function(x As Integer, y As Integer) x * y
Hey, that’s a lot less code. I was getting pretty tired of typing Integer over and over again anyway. The code works because Visual Basic looked at what you assigned to mult
and correctly identified its strong data type. In this case, mult
is of type Function(Integer, Integer) As Integer
(see Figure 9-1). It even correctly guessed the return type.
This code assumes that you have Option Infer
set to On
in your source code, or through the Project
properties (it’s the default). Chapter 6 discusses this option.
We could have shortened the mult
definition up even more.
Dim mult = Function(x, y) x * y
In this line, Visual Basic would infer the same function, but it would use the Object
data type throughout instead of Integer
. Also, if you have Option Strict
set to On
(which you should), this line will not compile until you add the appropriate As
clauses.
Internally, the Visual Basic compiler changes a lambda expression into an “expression tree,” a hierarchical structure that associates operands with their operators. Consider this semicomplex lambda expression that raises a multiplied expression to a power:
Dim calculateIt = Function(x, y, z) (x * y) ^ z
Visual Basic generates an expression tree for calculateIt
that looks like Figure 9-2.
When it comes time to use a lambda expression, Visual Basic traverses the tree, calculating values from the lower levels up to the top. These expression trees are stored as objects based on classes in the System.Linq.Expressions
namespace. If you don’t like typing lambda expressions, you can build up your own expression trees using these objects. However, my stomach is rumbling even more, so I’m going to leave that out of the book.
Although lambda expressions can’t contain Visual Basic statements such as For...Next
loops, you can still build up some pretty complex calculations using standard operators. Calls out to other functions can also appear in lambdas. In this code sample, mult
defers its work to the MultiplyIt
function:
Private Sub DoSomeMultiplication( ) Dim mult = Function(x As Integer, y As Integer) _ MultiplyIt(x, y) + 10 MsgBox(mult(5, 6)) ' Displays 40 End Sub Public Function MultiplyIt(ByVal x As Integer, _ ByVal y As Integer) As Integer Return x * y End Function
That’s pretty straightforward. But things get more interesting when you have lambda expressions that return other lambda expressions. Lambda calculus was invented partially to see how any complex function could be broken down into the most basic of functions. Even literal values can be defined as lambdas. Here’s the lambda expression that always returns the value 3
:
Dim three = Function( ) 3
You’ve already seen lambda expressions that accept more than one argument:
Dim mult1 = Function(x As Integer, y As Integer) x * y
In lambda calculus, this can be broken down into smaller functionettes, where each includes only a single argument:
Dim mult2 = Function(x As Integer) Function(y As Integer) x * y
The data type of mult2
is not exactly the same as mult1
’s data type, but they both generate the same answer from the same x and y values. When you use mult1
, it calculates the product of x and y and returns it. When you use mult2
, it first runs the Function(x As Integer)
part, which returns another lambda calculated by passing the value of x into its definition. If you pass in “5” as the value for x, the returned lambda is:
Function(y As Integer) 5 * y
This lambda is then calculated, and the product of 5 and y is returned. Calling mult2
in code is also slightly different. You don’t pass in both arguments at once. Instead, you pass in the argument for x, and then pass y to the returned initial lambda.
MsgBox(mult2(5)(6))
When run, the mult2(5)
part gets replaced with the first returned lambda. Then that first returned lambda is processed using (6)
as its y argument. Isn’t that simple? Well, no, it isn’t. And that’s OK, since the two-argument mult1
works just fine. The important part to remember is that it’s possible to build complex lambda expressions up from more basic lambda expressions. Visual Basic will use this fact when it generates the code for your LINQ-related expressions. We’ll talk more about it in Chapter 17, but even then, Visual Basic will manage a lot of the LINQ-focused lambda expressions for you behind the scenes.
Although you can pass arguments into a lambda expression, you may also use other variables that are within the scope of the lambda expression.
Private Sub NameMyChild( ) Dim nameLogic = GetChildNamingLogic( ) MsgBox(nameLogic("John")) ' Displays: Johnson End Sub Private Function GetChildNamingLogic( ) As _ Func(Of String, String) Dim nameSuffix As String = "son" Dim newLogic = Function(baseName As String) _ baseName & nameSuffix Return newLogic End Function
The GetChildNamingLogic
function returns a lambda expression. That lambda expression is used in the NameMyChild
method, passing John
as an argument to the lambda. And it works. The question is how. The problem is that nameSuffix
, used in the lambda expression’s logic, is a local variable within the GetChildNamingLogic
method. All local variables are destroyed whenever a method exits. By the time the MsgBox
function is called, nameSuffix
will be long gone. Yet the code works as though nameSuffix
lived on.
To make this code work, Visual Basic uses a new feature called variable lifting. Seeing that nameSuffix
will be accessed outside the scope of GetChildNamingLogic
, Visual Basic rewrites your source code, changing nameSuffix
from a local variable to a variable that has a wider scope.
In the new version of the source code, Visual Basic adds a closure class, a dynamically generated class that contains both the lambda expression and the local variables used by the expression. When you combine these together, any code that gets access to the lambda expression will also have access to the “local” variable.
Private Sub NameMyChild( ) Dim nameLogic = GetChildNamingLogic( ) MsgBox(nameLogic("John")) ' Displays: Johnson End Sub Public Class GeneratedClosureClass Public nameSuffix As String = "son" Public newLogic As Func(Of String, String) = _ Function(baseName As String) baseName & Me.nameSuffix End Class Private Function GetChildNamingLogic( ) As _ Func(Of String, String) Dim localClosure As New GeneratedClosureClass localClosure.nameSuffix = "son" Return localClosure.newLogic End Function
The actual code generated by Visual Basic is more complex than this, and it would include all of that function-delegate converted code I wrote about earlier. But this is the basic idea. Closure classes and variable lifting are essential features for lambda expressions since you can never really know where your lambda expressions are at all hours of the night.
To initialize object properties not managed by constructors, you need to assign those properties separately just after you create the class instance.
Dim newHire As New Employee newHire.Name = "John Doe" newHire.HireDate = #2/27/2008# newHire.Salary = 50000@
The With...End With
statement provides a little more structure.
Dim newHire As New Employee With newHire .Name = "John Doe" .HireDate = #2/27/2008# .Salary = 50000@ End With
A new syntax included in Visual Basic 2008 lets you combine declaration (with the New
keyword) and member assignment. The syntax includes a new variation of the With
statement.
Dim newHire As New Employee With { _ .Name = "John Doe", _ .HireDate = #2/27/2008#, _ .Salary = 50000@}
Well, as far as new features go, it’s not glitzy like lambda expressions or variable lifting. But it gets the job done.
Debugging and error processing are two of the most essential programming activities you will ever perform. There are three absolutes in life: death, taxes, and software bugs. Even in a relatively bug-free application, there is every reason to believe that a user will just mess things up royally. As a programmer, your job is to be the guardian of the user’s data as managed by the application, and to keep it safe, even from the user’s own negligence (or malfeasance), and also from your own source code.
I recently spoke with a developer from a large software company headquartered in Redmond, Washington; you might know the company. This developer told me that in any given application developed by this company, more than 50% of the code is dedicated to dealing with errors, bad data, system exceptions, and failures. Certainly, all this additional code slows down each application and adds a lot of overhead to what is already called “bloatware.” But in an age of hackers and data entry mistakes, such error management is an absolute must.
Testing—although not a topic covered in this book—goes hand in hand with error management. Often, the report of an error will lead to a bout of testing, but it should really be the other way around: testing should lead to the discovery of errors. A few years ago, NASA’s Mars Global Surveyor, in orbit around the red planet, captured images of the Beagle 2, a land-based research craft that crashed into the Martian surface in 2003. An assessment of the Beagle 2’s failure pinpointed many areas of concern, with a major issue being inadequate testing:
This led to an attenuated testing programme to meet the cost and schedule constraints, thus inevitably increasing technical risk. (From Beagle 2 ESA/UK Commission of Inquiry Report, April 5, 2004, Page 4)
Look at all those big words. Boy, the Europeans sure have a way with language. Perhaps a direct word-for-word translation into American English will make it clear what the commission was trying to convey:
They didn’t test it enough, and probably goofed it all up.
You will deal with three major categories of errors in your Visual Basic applications:
Some errors are so blatant that Visual Basic will refuse to compile your application. Generally, such errors are due to simple syntax issues that can be corrected with a few keystrokes. But you can also enable features in your program that will increase the number of errors recognized by the compiler. For instance, if you set Option Strict
to On
in your application or source code files, implicit narrowing conversions will generate compile-time errors.
' ----- Assume: Option Strict On Dim bigData As Long = 5& Dim smallData As Integer ' ----- The next line will not compile. smallData = bigData
Visual Studio 2008 includes features that help you locate and resolve compile-time errors. Such errors are marked with a “blue squiggle” below the offending syntax. Some errors also prompt Visual Studio to display corrective options through a pop-up window, as shown in Figure 9-3.
Runtime errors occur when a combination of data and code causes an invalid condition in what otherwise appears to be valid code. Such errors frequently occur when a user enters incorrect data into the application, but your own code can also generate runtime errors. Adequate checking of all incoming data will greatly reduce this class of errors. Consider the following block of code:
Public Function GetNumber( ) As Integer ' ----- Prompt the user for a number. ' Return zero if the user clicks Cancel. Dim useAmount As String ' ----- InputBox returns a string with whatever ' the user types in. useAmount = InputBox("Enter number.") If (IsNumeric(useAmount) = True) Then ' ----- Convert to an integer and return it. Return CInt(useAmount) Else ' ----- Invalid data. Return zero. Return 0 End If End Function
This code looks pretty reasonable, and in most cases, it is. It prompts the user for a number, converts valid numbers to integer format, and returns the result. The IsNumeric
function will weed out any invalid non-numeric entries. Calling this function will, in fact, return valid integers for entered numeric values, and 0 for invalid entries.
But what happens when a fascist dictator tries to use this code? As history has shown, a fascist dictator will enter a value such as “342304923940234.” Because it’s a valid number, it will pass the IsNumeric
test with flying colors, but since it exceeds the size of the Integer
data type, it will generate the dreaded runtime error shown in Figure 9-4.
Without additional error-handling code or checks for valid data limits, the GetNumber
routine generates this runtime error, and then causes the entire program to abort. Between committing war crimes and entering invalid numeric values, there seems to be no end to the evil that fascist dictators will do.
Logic errors are the third, and the most insidious, type of error. They are caused by you, the programmer; you can’t blame the user on this one. From process-flow issues to incorrect calculations, logic errors are the bane of software development, and they result in more required debugging time than the other two types of errors combined.
Logic errors are too personal and too varied to directly address in this book. You can force many logic errors out of your code by adding sufficient checks for invalid data, and by adequately testing your application under a variety of conditions and circumstances.
You won’t have that much difficulty dealing with compile-time errors. A general understanding of Visual Basic and .NET programming concepts, and regular use of the tools included with Visual Studio 2008, will help you quickly locate and eliminate them.
The bigger issue is: what do you do with runtime errors? Even if you check all possible data and external resource conditions, it’s impossible to prevent all runtime errors. You never know when a network connection will suddenly go down, or the user will trip over the printer cable, or a scratch on a DVD will generate data corruption. Anytime you deal with resources that exist outside your source code, you are taking a chance that runtime errors will occur.
Figure 9-4 showed you what Visual Basic does when it encounters a runtime error: it displays to the user a generic error dialog, and offers a chance to ignore the error (possible corruption of any unsaved data) or exit the program immediately (complete loss of any unsaved data).
Although both of these user actions leave much to the imagination, they don’t instill consumer confidence in your coding skills. Trust me on this: the user will blame you for any errors generated by your application, even if the true problem was far removed from your code.
Fortunately, Visual Basic includes three tools to help you deal completely with runtime errors, if and when they occur. These three Visual Basic features—unstructured error handling, structured error handling, and unhandled error handling—can all be used in any Visual Basic application to protect the user’s data—and the user—from unwanted errors.
Unstructured error handling has been a part of Visual Basic since it first debuted in the early 1990s. It’s simple to use, catches all possible errors in a block of code, and can be enabled or disabled as needed. By default, methods and property procedures include no error handling at all, so you must add error-handling code—unstructured or structured—to every routine where you feel it is needed.
The idea behind unstructured error handling is pretty basic. You simply add a line in your code that says, “If any errors occur at all, temporarily jump down to this other section of my procedure where I have special code to deal with it.” This “other section” is called the error handler.
Public Sub ErrorProneRoutine( ) ' ----- Any code you put here before enabling the ' error handler should be pretty resistant to ' runtime errors. ' ----- Turn on the error handler. On Error GoTo ErrorHandler ' ----- More code here with the risk of runtime errors. ' When all logic is complete, exit the routine. Return ErrorHandler: ' ----- When an error occurs, the code temporarily jumps ' down here, where you can deal with it. When you're ' finished, call this statement: Resume ' ----- which will jump back to the code that caused ' the error. The "Resume" statement has a few ' variations available. If you don't want to go ' back to main code, but just want to get out of ' this routine as quickly as possible, call: Return End Sub
The On Error
statement enables or disables error handling in the routine. When an error occurs, Visual Basic places the details of that error in a global Err
object. This object stores a text description of the error, the numeric error code of the error (if available), related online help details, and other error-specific values. I’ll list the details a little later.
You can include as many On Error
statements in your code as you want, and each one could direct errant code to a different label. You could have one error handler for network errors, one for file errors, one for calculation errors, and so on. Or you could have one big error handler that uses If...Then...Else
statements to examine the error condition stored in the global Err
object.
ErrorHandler: If (Err.Number = 5) Then ' ----- Handle error-code-5 issues here.
You can find specific error numbers for common errors in the online documentation for Visual Studio, but it is this dependence on hardcoded numbers that makes unstructured error handling less popular today than it was before .NET. Still, you are under no obligation to treat errors differently based on the type of error. As long as you can recover from error conditions reliably, it doesn’t always matter what the cause of the error was. Many times, if I have enabled error handling where it’s not the end of the world if the procedure reaches the end in an error-free matter, I simply report the error details to the user, and skip the errant line.
Public Sub DoSomeWork( ) On Error GoTo ErrorHandler ' ----- Logic code goes here. Return ErrorHandler: MsgBox("An error occurred in 'DoTheWork':" & _ Err.Description) Resume Next End Sub
This block of code reports the error, and then uses the Resume Next
statement (a variation of the standard Resume
statement) to return to the code line immediately following the one that caused the error. Another option uses Resume some_other_label
, which returns control to some specific named area of the code.
Using On Error GoTo
enables a specific error handler. Although you can use a second On Error GoTo
statement to redirect errors to another error handler in your procedure, a maximum of one error handler can be in effect at any moment. Once you have enabled an error handler, it stays in effect until the procedure ends, you redirect errors to another handler, or you specifically turn off error handling in the routine. To take this last route, issue the following statement:
On Error GoTo 0
Your error handler doesn’t have to do anything special. Consider this error-handling block:
ErrorHandler: Resume Next
When an error occurs, this handler immediately returns control to the line just following the one that generated the error. Visual Basic includes a shortcut for this action.
On Error Resume Next
By issuing the On Error Resume Next
statement, all errors will populate the Err
object (as is done for all errors, no matter how they are handled), and then skip the line generating the error. The user will not be informed of the error, and will continue to use the application in an ignorance-is-bliss stupor.
Unstructured error handling was the only method of error handling available in Visual Basic before .NET. Although it was simple to use, it didn’t fulfill the hype that surrounded the announcement that the 2002 release of Visual Basic .NET would be an object-oriented programming (OOP) system. Therefore, Microsoft also added structured error handling to the language, a method that uses standard objects to communicate errors, and error-handling code that is more tightly integrated with the code it monitors.
This form of error processing uses a multiline Try...Catch...Finally
statement to catch and handle errors.
Try ' ----- Add error-prone code here. Catch ex As Exception ' ----- Error-handling code here. Finally ' ----- Cleanup code goes here. End Try
Try
statements are designed to monitor smaller chunks of code. Although you could put all the source code for your procedure within the Try
block, it’s more common to put within that section only the statements that are likely to generate errors.
Try My.Computer.FileSystem.RenameFile(existingFile, newName) Catch...
“Safe” statements can remain outside the Try
portion of the Try...End Try
statement. Exactly what constitutes a “safe” programming statement is a topic of much debate, but two types of statements are generally unsafe: (1) those statements that interact with external systems, such as disk files, network or hardware resources, or even large blocks of memory; and (2) those statements that could cause a variable or expression to exceed the designed limits of the data type for that variable or expression.
The Catch
clause defines an error handler. As with unstructured error handling, you can include one global error handler in a Try
statement, or you can include multiple handlers for different types of errors. Each handler includes its own Catch
keyword.
Catch ex As ErrorClass
The ex
identifier provides a variable name for the active error object that you can use within the Catch
section. You can give it any name you wish; it can vary from Catch
clause to Catch
clause, but it doesn’t have to.
ErrorClass
identifies an exception class, a special class specifically designed to convey error information. The most generic exception class is System.Exception
; other, more specific exception classes derive from System.Exception
. Since Try...End Try
implements “object-oriented error processing,” all the errors must be stored as objects. The .NET Framework includes many predefined exception classes already derived from System.Exception
that you can use in your application. For instance, System.DivideByZeroException
catches any errors that (obviously) stem from dividing a number by zero.
Try result = firstNumber / secondNumber Catch ex As System.DivideByZeroException MsgBox("Divide by zero error.") Catch ex As System.OverflowException MsgBox("Divide resulting in an overflow.") Catch ex As System.Exception MsgBox("Some other error occurred.") End Try
When an error occurs, your code tests the exception against each Catch
clause until it finds a matching class. The Catch
clauses are examined in order from top to bottom, so make sure you put the most general one last; if you put System.Exception
first, no other Catch
clauses in that Try
block will ever trigger because every exception matches System.Exception
. How many Catch
clauses you include, or which exceptions they monitor, is up to you. If you leave out all Catch
clauses completely, it will act somewhat like an On Error Resume Next
statement, although if an error does occur, all remaining statements in the Try
block will be skipped. Execution continues with the Finally
block, and then with the code following the entire Try
statement.
The Finally
clause represents the “do this or die” part of your Try
block. If an error occurs in your Try
statement, the code in the Finally
section will always be processed after the relevant Catch
clause is complete. If no error occurs, the Finally
block will still be processed before leaving the Try
statement. If you issue a Return
statement somewhere in your Try
statement, the Finally
block will still be processed before leaving the routine. (This is getting monotonous.) If you use the Exit Try
statement to exit the Try
block early, the Finally
block is still executed. If, while your Try
block is being processed, your boss announces that a free catered lunch is starting immediately in the big meeting room and everyone is welcome, the Finally
code will also be processed, but you might not be there to see it.
Finally
clauses are optional, so you include one only when you need it. The only time that Finally
clauses are required is when you omit all Catch
clauses in a Try
statement.
I showed you earlier in the chapter how unhandled errors can lead to data corruption, crashed applications, and spiraling, out-of-control congressional spending. All good programmers understand how important error-handling code is, and they make the extra effort of including either structured or unstructured error-handling code. Yet there are times when I, even I, as a programmer, think, “Oh, this procedure isn’t doing anything that could generate errors. I’ll just leave out the error-handling code and save some typing time.” And then it strikes, seemingly without warning: an unhandled error. Crash! Burn! Another chunk of user data confined to the bit bucket of life.
Normally, all unhandled errors “bubble up” the call stack, looking for a procedure that includes error-handling code. For instance, consider this code:
Private Sub Level1( ) On Error GoTo ErrorHandler Level2( ) Return ErrorHandler: MsgBox("Error Handled.") Resume Next End Sub Private Sub Level2( ) Level3( ) End Sub Private Sub Level3( ) ' ----- The Err.Raise method forces an ' unstructured-style error. Err.Raise(1) End Sub
When the error occurs in Level3
, the application looks for an active error handler in that procedure, but finds nothing. So, it immediately exits Level3
and returns to Level2
, where it looks again for an active error handler. Such a search will, sadly, be fruitless. Heartbroken, the code leaves Level2
and moves back to Level1
, continuing its search for a reasonable error handler. This time it finds one. Processing immediately jumps down to the ErrorHandler
block and executes the code in that section.
If Level1
didn’t have an error handler, and no code farther up the stack included an error handler, the user would see the Error Message Window of Misery (refer to Figure 9-4), followed by the Dead Program of Disappointment.
Fortunately, Visual Basic does support a “catchall” error handler that traps such unmanaged exceptions and lets you do something about them. This feature works only if you have the “Enable application framework” field selected on the Application tab of the project properties. To access the code template for the global error handler, click the View Application Events button on that same project properties tab. Select “(MyApplication Events)” from the Class Name drop-down list above the source code window, and then select UnhandledException from the Method Name list. The following procedure appears in the code window:
Private Sub MyApplication_UnhandledException( _ ByVal sender As Object, _ ByVal e As Microsoft.VisualBasic. _ ApplicationServices.UnhandledExceptionEventArgs) _ Handles Me.UnhandledException End Sub
Add your special global error-handling code to this routine. The e
event argument includes an Exception
member that provides access to the details of the error via a System.Exception
object. The e.ExitApplication
member is a Boolean
property that you can modify either to continue or to exit the application. By default, it’s set to True
, so modify it if you want to keep the program running.
Even when the program does stay running, you will lose the active event path that triggered the error. If the error stemmed from a click on some button by the user, that entire Click
event, and all of its called methods, will be abandoned immediately, and the program will wait for new input from the user.
In addition to simply watching for them and screaming “Error!” there are a few other things you should know about error management in Visual Basic programs.
Believe it or not, there are times when you might want to generate runtime errors in your code. In fact, many of the runtime errors you encounter in your code occur because Microsoft wrote code in the Framework Class Libraries (FCLs) that specifically generates errors. This is by design.
Let’s say that you had a class property that was to accept only percentage values from 0 to 100, but as an Integer
data type.
Private StoredPercent As Integer Public Property InEffectPercent( ) As Integer Get Return StoredPercent End Get Set(ByVal value As Integer) StoredPercent = value End Set End Property
Nothing is grammatically wrong with this code, but it will not stop anyone from setting the stored percent value to either 847 or −847, both outside the desired range. You can add an If
statement to the Set
accessor to reject invalid data, but properties don’t provide a way to return a failed status code. The only way to inform the calling code of a problem is to generate an exception.
Set(ByVal value As Integer) If (value < 0) Or (value > 100) Then Throw New ArgumentOutOfRangeException("value", _ value, "The allowed range is from 0 to 100.") Else StoredPercent = value End If End Set
Now, attempts to set the InEffectPercent
property to a value outside the 0-to-100 range will generate an error, an error that can be caught by On Error
or Try...Catch
error handlers. The Throw
statement accepts a System.Exception
(or derived) object as its argument, and sends that exception object up the call stack on a quest for an error handler.
Similar to the Throw
statement is the Err.Raise
method. It lets you generate errors using a number-based error system more familiar to Visual Basic 6.0 and earlier environments. I recommend that you use the Throw
statement, even if you employ unstructured error handling elsewhere in your code.
You are free to mix both unstructured and structured error-handling methods broadly in your application, but a single procedure or method may use only one of these methods. That is, you may not use both On Error
and Try...Catch...Finally
in the same routine. A routine that uses On Error
may call another routine that uses Try...Catch...Finally
with no problems.
Now you may be thinking to yourself, “Self, I can easily see times when I would want to use unstructured error handling, and other times when I would opt for the more structured approach.” It all sounds very reasonable, but let me warn you in advance that there are error-handling zealots out there who will ridicule you for decades if you ever use an On Error
statement in your code. For these programmers, “object-oriented purity” is essential, and any code that uses nonobject methods to achieve what could be done through an OOP approach must be destroyed.
I’m about to use a word that I forbid my elementary-school-aged son to use. If you have tender ears, cover them now, though it won’t protect you from seeing the word on the printed page.
Rejecting the On Error
statement like this is just plain stupid. As you may remember from earlier chapters, everything in your .NET application is object-oriented, since all the code appears in the context of an object. If you are using unstructured error handling, you can still get to the relevant exception object through the Err.GetException( )
method, so it’s not really an issue of objects.
Determining when to use structured or unstructured error handling is no different from deciding to use C# or Visual Basic to write your applications. For most applications, the choice is irrelevant. One language may have some esoteric features that may steer you in that direction (such as optional method arguments in Visual Basic), but the other 99.9% of the features are pretty much identical.
The same is true of error-handling methods. There may be times when one is just plain better than the other. For instance, consider the following code that calls three methods, none of which includes its own error handler:
On Error Resume Next RefreshPart1( ) RefreshPart2( ) RefreshPart3( )
Clearly, I don’t care whether an error occurs in one of the routines or not. If an error causes an early exit from RefreshPart1
, the next routine, RefreshPart2
, will still be called, and so on. I often need more diligent error-checking code than this, but in low-impact code, this is sufficient. To accomplish the same thing using structured error handling would be a little more involved.
Try RefreshPart1( ) Catch End Try Try RefreshPart2( ) Catch End Try Try RefreshPart3( ) Catch End Try
That’s a lot of extra code for the same functionality. If you’re an On Error
statement hater, by all means use the second block of code. But if you are a more reasonable programmer, the type of programmer who would read a book such as this, use each method as it fits into your coding design.
The System.Exception
class is the base class for all structured exceptions. When an error occurs, you can examine its members to determine the exact nature of the error. You also use this class (or one of its derived classes) to build your own custom exception in anticipation of using the Throw
statement. Table 9-1 lists the members of this object.
Table 9-1. Members of the System.Exception class
Object member | Description |
---|---|
| Provides access to a collection of key-value pairs, each providing additional exception-specific information. |
| Identifies online help location information relevant to this exception. |
| If an exception is a side effect of another error, the original error appears here. |
| A textual description of the error. |
| Identifies the name of the application or object that caused the error. |
| Returns a string that fully documents the current stack trace, the list of all active procedure calls that led to the statement causing the error. |
| Identifies the name of the method that triggered the error. |
Classes derived from System.Exception
may include additional properties that provide additional detail for a specific error type.
The Err
object provides access to the most recent error through its various members. Anytime an error occurs, Visual Basic documents the details of the error in this object’s members. It’s often accessed within an unstructured error handler to reference or display the details of the error. Table 9-2 lists the members of this object.
Table 9-2. Members of the Err object
Description | |
---|---|
| Clear all the properties in the |
| A textual description of the error. |
| The line number label nearest to where the error occurred. In modern Visual Basic applications, numeric line labels are almost never used, so this field is generally 0. |
| The location within an online help file relevant to the error. If this property and the |
| The online help file related to the active error. |
| The numeric return value from the most recent call to a pre-.NET DLL, whether it is an error or not. |
| The numeric code for the active error. |
| Use this method to generate a runtime error. Although this method does include some arguments for setting other properties in the |
| The name of the application, class, or object that generated the active error. |
Visual Basic 6.0 (and earlier) included a handy tool that would quickly output debug information from your program, displaying such output in the “Immediate Window” of the Visual Basic development environment.
Debug.Print "Reached point G in code"
The .NET version of Visual Basic enhances the Debug
object with more features, and a slight change in syntax. The Print
method is replaced with WriteLine
; a separate Write
method outputs text without a final carriage return.
Debug.WriteLine("Reached point G in code")
Everything you output using the WriteLine
(or similar) method goes to a series of “listeners” attached to the Debug
object. You can add your own listeners, including output to a work file. But the Debug
object is really used only when debugging your program. Once you compile a final release, none of the Debug
-related features works anymore, by design.
If you wish to log status data from a released application, consider using the My.Application.Log
object instead (or My.Log
in ASP.NET programs). Similar to the Debug
object, the Log
object sends its output to any number of registered listeners. By default, all output goes to the standard debug output (just like the Debug
object) and to a logfile created specifically for your application’s assembly. See the online help for the My.Application.Log
object for information on configuring this object to meet your needs.
The Visual Basic language includes a few other error-specific statements and features that you may find useful:
ErrorToString
functionThis method returns the error message associated with a numeric system error code. For instance, ErrorToString(10)
returns “This array is fixed or temporarily locked.” It is useful only with older unstructured error codes.
IsError
functionWhen you supply an object argument to this function, it returns True
if the object is a System.Exception
(or derived) object.
The best program in the world would never generate errors, I guess. But come on, it’s not reality. If a multimillion-dollar Mars probe is going to crash on a planet millions of miles away, even after years of advanced engineering, my customer-tracking application for a local video rental shop is certainly going to have a bug or two. But you can mitigate the impact of these bugs using the error-management features included with Visual Basic.
This chapter’s project code will be somewhat brief. Error-handling code will appear throughout the entire application, but we’ll add it in little by little as we craft the project. For now, let’s just focus on the central error-handling routines that will take some basic action when an error occurs anywhere in the program. As for lambda expressions, we’ll hold off on such code until a later chapter.
As important and precise as error handling needs to be, the typical business application will not encounter a large variety of error types. Applications such as the Library Project are mainly vulnerable to three types of errors: (1) data entry errors; (2) errors that occur when reading data from, or writing data to, a database table; and (3) errors related to printing. Sure, there may be numeric overflow errors or other errors related to in-use data, but it’s mostly interactions with external resources, such as the database, that concern us.
Because of the limited types of errors occurring in the application, it’s possible to write a generic routine that informs the user of the error in a consistent manner. Each time a runtime error occurs, we will call this central routine, just to let the user know what’s going on. The code block where the error occurred can then decide whether to take any special compensating action, or continue on as though no error occurred.
Load the Chapter 9 (Before) Code project, either through the New Project templates or by accessing the project directly from the installation directory. To see the code in its final form, load Chapter 9 (After) Code instead.
In the project, open the General.vb class file, and add the following code as a new method to Module General
.
Insert Chapter 9, Snippet Item 1.
Public Sub GeneralError(ByVal routineName As String, _ ByVal theError As System.Exception) ' ----- Report an error to the user. MsgBox("The following error occurred at location '" & _ routineName & "':" & vbCrLf & vbCrLf & _ theError.Message, _ MsgBoxStyle.OKOnly Or MsgBoxStyle.Exclamation, _ ProgramTitle) End Sub
Not much to that code, is there? So, here’s how it works. When you encounter an error in some routine, the in-effect error handler calls the central GeneralError
method.
Public Sub SomeRoutine( ) On Error GoTo ErrorHandler ' ----- Lots of code here. Return ErrorHandler: GeneralError("SomeRoutine", Err.GetException( )) Resume Next End Sub
You can use it with structured errors as well.
Try ' ----- Troubling code here. Catch ex As System.Exception GeneralError("SomeRoutine", ex) End Try
The purpose of the GeneralError
global method is simple: communicate to the user that an error occurred, and then move on. It’s meant to be simple, and it is simple. You could enhance the routine with some additional features. Logging of the error out to a file (or any other active log listener) might assist you later if you needed to examine application-generated errors. Add the following code to the routine, just after the MsgBox
command, to record the exception.
Insert Chapter 9, Snippet Item 2.
My.Application.Log.WriteException(theError)
Of course, if an error occurs while writing to the log, that would be a big problem, so add one more line to the start of the GeneralError
routine.
Insert Chapter 9, Snippet Item 3.
On Error Resume Next
As I mentioned earlier, it’s a good idea to include a global error handler in your code, in case some error gets past your defenses. To include this code, display all files in the Solution Explorer using the Show All Files button, open the ApplicationEvents.vb file, and add the following code to the MyApplication
class.
Insert Chapter 9, Snippet Item 4.
Private Sub MyApplication_UnhandledException( _ ByVal sender As Object, ByVal e As Microsoft. _ VisualBasic.ApplicationServices. _ UnhandledExceptionEventArgs) Handles _ Me.UnhandledException ' ----- Record the error, and keep running. e.ExitApplication = False GeneralError("Unhandled Exception", e.Exception) End Sub
Since we already have the global GeneralError
routine to log our errors, we might as well take advantage of it here.
That’s it for functional and error-free programming. In the next chapter, which covers database interactions, we’ll make frequent use of this error-handling code.
3.15.18.198