Share via


Functional Programming

This chapter is excerpted from Programming Visual Basic 2008: Build .NET 3.5 Applications with Microsoft's RAD Tool for Business by Tim Patrick, published by O'Reilly Media

Programming Visual Basic 2008

Logo

Buy Now

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

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, LINQ). 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!

Implying Lambdas

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, Data and Data Types, 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, "Visual Basic is also good at playing 20 questions"). It even correctly guessed the return type.

Figure 9.1. Visual Basic is also good at playing 20 questions

Visual Basic is also good at playing 20 questions

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, Data and Data Types 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.

Expression Trees

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, "Expression trees group operands by operator".

Figure 9.2. Expression trees group operands by operator

Expression trees group operands by operator

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.

Complex Lambdas

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, LINQ, but even then, Visual Basic will manage a lot of the LINQ-focused lambda expressions for you behind the scenes.

Variable Lifting

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.

Object Initializers

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.

Error Handling in Visual Basic

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.

The Nature of Errors in Visual Basic

You will deal with three major categories of errors in your Visual Basic applications:

  • Compile-time errors
    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, "Error correction options for a narrowing conversion".

    Figure 9.3. Error correction options for a narrowing conversion

    Error correction options for a narrowing conversion

  • Runtime errors
    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, "An error message only a fascist dictator could love".

    Figure 9.4. An error message only a fascist dictator could love

    An error message only a fascist dictator could love

    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
    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, "An error message only a fascist dictator could love" 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

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.

Disabling Error Handling

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

Ignoring Errors

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.

Structured Error Handling

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

The Try Clause

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

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

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.

Unhandled Errors

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, "An error message only a fascist dictator could love"), 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.

Managing Errors

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.

Generating Errors

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.

Mixing Error-Handling Methods

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.

Warning

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

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, "Members of the System.Exception class" lists the members of this object.

Table 9.1. Members of the System.Exception class

Object member

Description

Data property

Provides access to a collection of key-value pairs, each providing additional exception-specific information.

HelpLink property

Identifies online help location information relevant to this exception.

InnerException property

If an exception is a side effect of another error, the original error appears here.

Message property

A textual description of the error.

Source property

Identifies the name of the application or object that caused the error.

StackTrace property

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.

TargetSite property

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

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, "Members of the Err object" lists the members of this object.

Table 9.2. Members of the Err object

Object member

Description

Clear method

Clear all the properties in the Err object, setting them to their default values. Normally, you use the Err object only to determine the details of a triggered error. But you can also use it to initiate an error with your own error details. See the description of the Raise method later in the table.

Description property

A textual description of the error.

Erl property

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.

HelpContext property

The location within an online help file relevant to the error. If this property and the HelpFile property are set, the user can access relevant online help information.

HelpFile property

The online help file related to the active error.

LastDLLError property

The numeric return value from the most recent call to a pre-.NET DLL, whether it is an error or not.

Number property

The numeric code for the active error.

Raise method

Use this method to generate a runtime error. Although this method does include some arguments for setting other properties in the Err object, you can also set the properties yourself before calling the Raise method. Any properties you set will be retained in the object for examination by the error-handler code that receives the error.

Source property

The name of the application, class, or object that generated the active error.

The Debug Object

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.

Other Visual Basic Error Features

The Visual Basic language includes a few other error-specific statements and features that you may find useful:

  • ErrorToStringfunction
    This 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.

  • IsErrorfunction
    When you supply an object argument to this function, it returns True if the object is a System.Exception (or derived) object.

Summary

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.

Project

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.

General Error Handler

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.

Note

Load the Chapter 9, Functional Programming (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, Functional Programming (After) Code instead.

In the project, open the General.vb class file, and add the following code as a new method to Module General.

Note

Insert Chapter 9, Functional Programming, 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.

Note

Insert Chapter 9, Functional Programming, 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.

Note

Insert Chapter 9, Functional Programming, Snippet Item 3.

On Error Resume Next

Unhandled Error Capture

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.

Note

Insert Chapter 9, Functional Programming, 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.