Page #52 (Chapter 6 - WebClass Debugging and Error Handling)

Chapter 6 - WebClass Debugging and Error Handling

Visual Basic Developers Guide to ASP and IIS
A. Russell Jones
  Copyright 1999 SYBEX Inc.

Trapping Errors
If your job involves developing COM (ActiveX) objects or controls for use by other programmers in varying environments, then you'll probably be well aware of the error-handling requirements. Basically, you're creating a "black box" component that must be able to communicate with other people's code, must adhere to the requirements of its environment (in this case, the Web server), and must degrade gracefully if its own requirements are not met.
For example, in a personal application, the fatal error message box is often enough error handling. If you wrote the app, and you're running it on your own machine, you can probably re-create the situation and fix the problem. When you're running in client-server mode and your client-side app fails, it at least affects only one person—the person working on the client computer. But if your WebClass fails, you could conceivably affect hundreds of people simultaneously. In the worst of situations, you could potentially also affect all other applications running on the Web server, by taking up bandwidth or even crashing the server.
That's why many ISPs won't install WebClass-based applications on their servers. In contrast, ASP applications run in the scripting environment. You can certainly slow down the server by writing endless loops inside an ASP file, but it's difficult to crash the server altogether. Although you may not be able to run your WebClass applications on an ISP's server, you have the same problems on your own servers.
The answer is error trapping. You're trying to trap several types of problems, and it's good to keep them in mind right up front:
  Parameter/input errors
  Logic errors
  Errors in external code
  Resource errors
Your goals in trapping these types of errors should be, in order of importance, to:
  1. Keep your application from crashing
  2. Provide as much information as possible about the error
  3. Propagate all errors back to a critical decision point
  4. Degrade gracefully
If your application crashes, you've lost the game. You won't be able to accomplish any of the other three goals. The more information you provide about the error, the easier it will be for you to find and fix it. You don't want your application to stop—even with good error messages—if you can work around the error, so you'll want to categorize errors based on how they affect your application. For example, input errors should never be fatal—you should trap them all and provide reasonable error messages so that the user or programmer providing the information can solve the problem. During development, you should focus primarily on the first two goals. In some cases, you won't have enough information to do more than approach the third goal during initial development. Later in the development cycle, after several users have tested your application, you should be able to improve your error handling to solve common problems. Spending lots of time degrading gracefully does no good if that causes you to miss fatal errors that crash the application.
Your first line of defense in a WebClass application, as in any application, is to decide which type of error handling is appropriate inside each routine. VB provides two error-handling methods: On Error GoTo and On Error Resume Next. Which one is best depends on what you're doing at the time. The overriding rule is that using either is better than using none. I routinely use On Error GoTo at the start of each routine, then change to On Error Resume Next for code that accesses external objects—those that aren't under my control.
Your biggest problems will arise when you don't have any error handling in a routine. Code that works every time in development will fail in production, sometimes due to outside factors.
Error-Handling Scenario
Consider this scenario: You have a recordset populated with data from a database query that you use to fill an HTML select list on the browser. You want to display a list of names associated with UserIDs. The user submits a form containing the UserID associated with the selection back to your application. What are the possibilities for failure?
  You could fail to obtain a recordset.
  The recordset could be empty.
  Critical data may be missing.
  Some of the fields could contain a null value.
  The user could submit the form without selecting an item.
  The item selected might not match an item in the database.
Generic error handlers will catch all these errors but may not provide enough information to solve the problem. Let's take some of these errors in turn and analyze them from the standpoint of each error type and your goals in trapping them.
You Could Fail to Obtain a Recordset
This is a call to external code. You can't control whether the connection to the database will be valid, whether the database server is running, or whether the network is running; therefore, you should trap the call with On Error Resume Next. You always test for errors after the call. If the call fails, you can raise an error in your routine. If you follow the guidelines in this section, your application will either decide what to do at that point, or it will propagate the error back to a critical decision point.
The following code illustrates one way to handle external errors:
Sub showSelectList
     Dim R as Recordset
     Dim methodName as string
     methodName = "showSelectList"
     On Error Resume Next
     Set R = conn.execute(someSQLStatement, ,adCmdText)
     If Err.Number <> 0 then
         Err.Raise Err.Number, Err.Source & ", " & _
         methodName, Err.Description & _
         "Error obtaining data " & _
         "for selection list."
     End If
     ' continue processing
End Sub
  Note The methodName variable is necessary because VB doesn't provide runtime access to the call stack. This is an irritating oversight that should have been remedied several versions ago. By appending the methodName to the source, you can log the call stack state without too much effort. You can also maintain a global array or stack object to store the state of the call stack, thus allowing you to log it when errors occur.
For external errors, use On Error Resume Next and test for errors when the call returns. If the external code raises an error, you'll catch it in the trap If Err.Number <> 0. You have two choices whenever you catch an error: You can decide how to handle the error at that point (create a critical error handler) or you can defer the decision to the calling code. Of course, you can also choose to ignore the error, but that's only advisable under certain circumstances, when you're expecting the error to occur and know exactly what you'll do if the error does occur. One such example is testing the upper bound of an array:
Function getUboundIntArray(anArray() as Integer) as long
   On Error Resume Next
   getUboundIntArray= Ubound(anArray)
End Function
The Recordset Could Be Empty
Treat this as a parameter or input error. If the external code returned a Recordset object, then it has performed successfully. It's your responsibility to check if the data returned is valid. Normally, your critical error handler for parameter or input errors is internal to the routine. You don't always want to raise an error for parameter/input validation failure—sometimes, you want to return a message. The deciding factor here is usually the type of person using your application. If the person calling your code is an end user, return an error code and an informative status message. The purpose is to enable the end user to communicate coherently with technical support so they can help solve the problem. Remember, in many cases you'll be the one supporting the application. If the user is another programmer using your DLL as a data source or functional component, return a detailed error message.
Sub showSelectList
     Dim R as Recordset
     Dim methodName as string
     methodName = "showSelectList"
     On Error Resume Next
     Set R = conn.execute(someSQLStatement, , adCmdText)
     If Err.Number <> 0 then
         Err.Raise Err.Number, Err.Source & methodName, _
         Err.Description & "Error obtaining data " & _
         "for selection list."
     End If
     If (R.EOF and R.BOF) then
         ' no data, do something
     End If
End Sub
Critical Data May Be Missing
At this point, you're reasonably sure that no error occurred while retrieving the recordset and that the recordset contains at least one row of data. Now you need to make sure that the recordset contains the fields that you need. I call errors like these resource errors because they stem from missing, unreachable, or invalid resources, which include file and database data, network connections, database connections, etc.
If you are a careful person, you will have requested only those fields in your SQL statement, so you can skip this step. If you're the kind of person who typically opens tables or uses SQL statements such as SELECT * FROM MyTable rather then executing specific SQL statements or stored procedures, then you can't be sure that the database hasn't changed. If the database administrator deleted a column, changed a data type, or renamed a column, the changes will affect your application. You'll either have to ensure that the anticipated columns exist or trap for the errors that will occur when you try to reference the fields. When you think about it this way, requesting exactly the fields you need from the database is a lot less work than trapping all the potential errors.
Sub showSelectList
     Dim R as Recordset
     Dim methodName as string
     Dim F as Field
     Dim requiredFields as integer
     methodName = "showSelectList"
     ' code to retrieve recordset
     ' …
     For Each F in R.Fields
         If F.Name = "UserID" or F.Name="LastName" _
             or F.Name="FirstName" then
             requiredFields = requiredFields + 1
         End If
     Next
     On Error Goto Err_showSelectList
     If requiredFields <> 3 then
         Err.Raise 50001, methodName, "A required field " _
             & "is missing in the list of user names."
     End If
     ' continue processing
Exit_showSelectList:
     Exit Sub
Err_showSelectList:
     Err.Raise Err.Number, methodName, Err.Description
End Sub
You're now sure that the required fields are present, but unless you wrote a specific SQL query, you don't know the order in which they appear in the recordset. If you use field index numbers to reference the fields, you may not get an error, but you won't get the desired result either. In other words, suppose the SELECT * query returns LastName, FirstName, and UserID. If your code is expecting UserID, FirstName, LastName, the code will fail. If you use explicit field names instead, the code will run as expected.
' This code will fail if the field order changes
Response.write "<select name='UserID'>
Do While Not R.EOF
     Response.write "<option value='" & R(0) & "'>" _
     & R(1) & " " & R(2) & "</option>"
     R.MoveNext
Loop
Response.Write "</select>"
' This code will run even if the field order changes
Response.write "<select name='lstUsers'>
Do While Not R.EOF
     Response.write "<option value='" & R("UserID") & _
         "'>" & R("FirstName") & " " & R("LastName") & _
         "</option>"
     R.MoveNext
Loop
Response.Write "</select>"
You have only one more check to make to avoid errors. Both of the loops to fill the select list will break if any data field contains a null value. Here's the fixed code:
Response.write "<select name='lstUsers'>"
With Response
     Do While Not R.EOF
         .write "<option value='"
         If Not isNull(R("UserID") then
             .Write R("UserID") & "'>"
         Else
             .Write "0" & "'>"
         End If
         If Not isNull(R("FirstName")) then
             .Write R("FirstName") & " "
         End If
         If Not isNull(R("LastName")) then
             .Write & R("LastName")
         Else
             .Write "N/A"
         End If
        .Write "</option>"
        R.MoveNext
     Loop
End With
Response.Write "</select>"
You have to be sure to activate a generic error handler for all code not explicitly wrapped in On Error Resume Next followed by an error test; otherwise, your code can fail.
     ' Activate internal code error handler
     On Error Goto Err_showSelectList
     ' process data here
Exit_showSelectList:
     Exit Sub
Err_showSelectList:
     Err.Raise Err.Number, methodName, Err.Description
End Sub
When the user submits the form, you need to ensure that the data you're expecting is present:
If isEmpty(Request.Form("UserID")) then
   ' return message to user
End If
One type of error specific to Web applications is caused by people being able to bookmark a place in your application. They can also create their own forms and submit them to your application—even in the wrong place. Although this may not be common for form data, it's extremely common with QueryString data. You need to perform a data-type check, just to be sure. Data stored in the Request object is always Variant data—and it's always string data. One way to ensure that you have the correct data type is to attempt to cast the variable to the correct type, and—you guessed it—trap the resulting error.
Dim vUserID as Variant
Dim lUserID as Long
vUserID = Request.Form("UserID")
If isEmpty(vUserID) then
   ' return message to user
End If
On Error Resume Next
LUserID = CLng(vUserID)
If Err.Number <> 0 then
   ' raise error
End If
Finally, after all that code, you're ready to do something with the data returned by the form.
The code you've just written is strong error-trapping. It meets the requirements for the first two goals of error trapping nicely. It will keep your application from crashing and provide reasonably good error messages, but it doesn't fulfill all the goals. Recall that goals 3 and 4 are to propagate all errors back to a critical decision point and to degrade gracefully.
Error propagation is the process of raising errors back up the call stack via installed error-handlers until you reach a point where you can logically decide, based on the error source and number, what you should do next. Propagation requires that you activate error handling in every routine in the call stack. A critical error-handler is a routine that can make a decision about what to do. The reason they're critical is that if you don't make the decision, VB will make it for you—every unhandled error is fatal, and your application quits. That's not a good response in most situations, and it's unacceptable for a server-based application.
Where should you create critical error-handlers? I believe that they properly belong at the beginning points of each state operation. You can think of your application as entering one state after another: "Now I'm initializing; now I'm displaying a form; now I'm retrieving data; now I'm validating input, etc." At the beginning of each state, you should be able to decide what to do if the operation fails. To continue the example, if you're retrieving a list of users and you can't make a database connection, you can simply write a message to the screen saying the data is unavailable. The point is, you have to make the decision somewhere. For me, the placement of these critical error-handlers is usually an iterative process. I start with critical error-handlers at the beginning of large operations and add more as the starting points of sub-states within the operations becomes clear. Experience will show you the best places to create them, but it's most important to realize that you have to have at least one at the beginning of each operation to keep your application from crashing.
The final goal is to degrade gracefully. Although any unhandled error can crash your application, you will want to respond to different trapped errors in different ways. In many cases, you can provide default values when data is missing. This is relatively easy to do for parameter/input errors but more difficult to do for resource errors. For example, if the database server is unavailable, it's usually impossible to substitute default values: "I don't know why last quarter's sales total is $0.00, sir." Often, you can defer the decision to the end user, for example, "Unable to communicate with the database. Do you want to try again?"
Logic errors are generally fatal, because you don't know why they occurred. The best thing you can do is to log as much information as possible so that you can find and fix the error. External code errors are the most dangerous. In the age of modular programming and million-line operating systems, you have to trust a lot of code just to get your application to run. If the API call to getSystemTime returns an incorrect time, you may not even know that an error occurred, possibly leading to many other problems.
How can you know that all your routines have active error traps, or that your critical error handlers are working properly? You can (almost) solve the problem with code reviews and testing. No one is immune from writing sloppy code sometimes. It's all too easy to write a few lines that you know are going to work (until they don't). For small applications, you can check the code, but you should always get someone else to test the application. A good rule of thumb is that five users will find 95 percent of the errors in your program. Unfortunately, it takes many more users to find the last 5 percent. Most programmers are aware that many bugs are never found—but that's not to say that they never cause problems.
You don't have to handle every error in each method. Although VB doesn't have an On Error Call <subroutine> name, you can solve part of the problem by writing generic error-handling, display, and/or logging routines. Here's a simple example that logs an error:
Sub testError()
     On Error Goto Err_testError
     Err.Raise 50000, "testError", "This is a test error."
Err_testError:
     Call LogError
     Exit Sub
End Sub
Sub LogError()
     App.LogEvent "Error #" & cstr(Err.Number) & _
          " Source: " & Err.Source & " Description: " _
          & Err.Description
     Err.Raise Err.Number, Err.Source, Err.Description  
End Sub
Usually VB clears the Err object when it encounters an Exit Sub, Exit Function, or Exit Property line, but when you call a function or subroutine inside an active error-handler, VB does not clear the Err object until it encounters a Resume statement. You can use this feature to process errors with generic routines and still have access to the error information when the generic routine exits.
I've taken you through this discussion because I want to make sure you realize that server applications are fundamentally different from stand-alone applications. Visual Basic is a powerful programming language that gives you direct access to the operating system kernel calls. You can easily cause serious problems by writing bad code or by failing to anticipate or trap errors. Conditions you may have taken for granted in stand-alone or standard client-server applications—permanent database connections, homogeneous client operating systems, and almost exclusive use of file resources—are no longer available in the server-based application space. Your application may not be the only one on the server. When the server crashes, bringing the company intranet down with it, your WebClass application had better not be the cause of the problem.



Visual Basic Developer[ap]s Guide to ASP and IIS
Visual Basic Developer[ap]s Guide to ASP and IIS
ISBN: 782125573
EAN: N/A
Year: 2005
Pages: 98

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net