When you call a Visual Basic routine that can fail, what is the standard way that the routine signals the failure to you? It probably won't be via a return value. If it were, procedures, for one, would have trouble signaling failure. Most (but not all) routines raise an error (an exception) when they want to signal failure. (This applies to procedures, functions, and methods.) For example, CreateObject raises an exception if it cannot create an object-for whatever reason; Open does the same if it cannot open a file for you. (Not all routines raise such exceptions. For example, the Choose function returns Null [thus, i 222y249c t requires a Variant to hold its return value just in case it ever fails] if you index into it incorrectly.) In other words, if a routine works correctly, this fact is signaled to the caller by the absence of any error condition.
Routines such as Open work like this primarily so that they can be used more flexibly. For example, by not handling the error internally, perhaps by prompting the user in some way, the caller is free to handle errors in the most suitable way. The caller can also use routines in ways perhaps not thought of by their writers. Listing 1-1 is an example using GetAttr to determine whether the machine has a particular drive. Because this routine raises exceptions, we can use it to determine whether or not a disk drive exists.
Listing 1-1 Using error handling to test for a disk drive
Public Function bDriveExists(ByVal sDriveAndFile As String) _ As Boolean ' Module: modFileUtilities. Function: bDriveExists. ' Object: General ' Author - Peter J. Morris. TMS Ltd. ' Template fitted : Date - 01/01/97 Time - 00:00 ' Function's Purpose/Description in Brief ' Determines whether the drive given in sDriveAndFile ' exists. Raises an exception if no drive string is given. ' Revision History: ' BY WHY AND WHEN AFFECTED ' Peter J. Morris. TMS Ltd. - Original Code 01/01/97, 00:00 ' INPUTS - sDriveAndFile - holds the drive name, e.g., "C". Later holds the name of the drive and the filename on the drive to be created. ' OUTPUTS - Via return. Boolean. True if drive exists; else False. ' MAY RAISE EXCEPTIONS ' NOTES: Uses formals as variables. Uses delayed error handling. ' Set up general error handler. On Error GoTo Error_General_bDriveExists: Const sProcSig = MODULE_NAME & "General_bDriveExists" ' ========== Body Code Starts ========== ' These are usually a little more public - shown local ' for readability only. Dim lErr As Long Dim lErl As Long Dim sErr As String ' Constants placed here instead of in typelib for ' readability only. Const nPATH_NOT_FOUND As Integer = 76 Const nINTERNAL_ERROR_START As Integer = 1000 Const nERROR_NO_DRIVE_CODE As Integer = 1001 ' Always default to failure. bDriveExists = False If sDriveAndFile <> "" Then ' "Trim" the drive name. sDriveAndFile = Left$(sDriveAndFile, 1) ' Root directory. sDriveAndFile = sDriveAndFile & ":\" ' Enter error-critical section - delay the handling ' of any possible resultant exception. On Error Resume Next Call VBA.FileSystem.GetAttr(sDriveAndFile) ' Preserve the error context. See notes later on ' subclassing VBA's error object and adding your own ' "push" and "pop" methods to do this. GoSub PreserveContext ' Exit error-critical section. On Error GoTo Error_General_bDriveExists: Select Case nErr Case nPATH_NOT_FOUND: bDriveExists = False ' Covers no error (error 0) and all other errors. ' As far as we're concerned, these aren't ' errors; e.g., "drive not ready" is OK. Case Else bDriveExists = True End Select Else ' No drive given, so flag error. Err.Raise nLoadErrorDescription(nERROR_NO_DRIVE_CODE) End If ' ========== Body Code Ends ========== Exit Function ' Error handler Error_General_bDriveExists: ' Preserve the error context. See notes later on ' subclassing VBA's error object and adding your own "push" ' and "pop" methods to do this. GoSub PreserveContext ' ** ' In error; roll back stuff in here. ' ** ' ** ' Log error. ' ** ' Reraise as appropriate - handle internal errors ' further only. If (lErr < nINTERNAL_ERROR_START) Or _ (lErr = nERROR_NO_DRIVE_CODE) Then VBA.Err.Raise lErr Else ' Ask the user what he or she wants to do with this ' error. Select Case MsgBox("Error in " & sProcSig & " " _ & CStr(lErr) & " " & _ CStr(lErl) & " " & sErr, _ vbAbortRetryIgnore + vbExclamation, _ sMsgBoxTitle) Case vbAbort Resume Exit_General_bDriveExists: Case vbRetry Resume Case vbIgnore Resume Next Case Else VBA.Interaction.MsgBox _ "Unexpected error" _ , vbOKOnly + vbCritical _ , "Error" End End Select End If Exit_General_bDriveExists: Exit Function PreserveContext: lErr = VBA.Err.Number lErl = VBA.Erl sErr = VBA.Err.Description Return End Function |
Here are a few comments on this routine:
Although it's a fabricated example, I've tried to make sure that it works and is complete.
It handles errors.
It uses delayed error handling internally.
It's not right for you! You'll need to rework the code and the structure to suit your particular needs and philosophy.
The error handler might raise errors.
It doesn't handle errors occurring in the error handler.
It uses a local subroutine, PreserveContext. This subroutine is called only from within this routine, so we use a GoSub to create it. The result is that PreserveContext is truly private and fast-and it doesn't pollute the global name space. (This routine preserves the key values found in the error object. Tip 11 explains a way to do this using a replacement Err object.)
Within bDriveExists, I've chosen to flag parameter errors and send the information back to the caller by using exceptions. The actual exception is raised using the Raise method of the Visual Basic error object (Err.Raise) and the return value of a function (nLoadErrorDescription). This return value is used to load the correct error string (typically held in string resources and not a database since you want to always be able to get hold of the string quickly). This string is placed into Err.Description just before the Raise method is applied to the error object. Reraising, without reporting, errors like this allows you to build a transaction model of error handling into your code. (See Tip 14 for more on this topic.)
The nLoadErrorDescription function is typically passed the error number (a constant telling it what string to load), and it returns this same number to the caller upon completion. In other words, the function could look something like this (omitting any boilerplate code):
Public Function nLoadErrorDescription(ByVal nCode As Integer)In this example, we're using a string resource to hold the error text. In reality, the routine we normally use to retrieve an error string (and, indeed, to resolve the constant) is contained in what we call a ROOS-that's a Resource Only OLE Server, which we'll come back to in Tip 10.
A good error handler is often complex, question: What will happen if we get an error in the error handler? Well, if we're in the same local scope as the original error, the error is passed back up the call chain to the next available error handler. (See Tip 5 for more information on the call chain and this mechanism.) In other words, if you're in the routine proper when this second error occurs, it will be handled "above" your routine; if that's Visual Basic, you're dead! "OK," you say, "to handle it more locally, I must have an error handler within my error handler." Sounds good-trouble is, it doesn't work as you might expect. Sure, you can have an On Error Goto xyz (or On Error Resume Next or On Error Resume 0) in your error handler, but the trap will not be set; your code will not jump to xyz if an error occurs in your error handler. The way to handle an error in your error handler is to do it in another procedure. If you call another procedure from your error handler, that routine can have an error trap set. The net effect is that you can have error handling in your error handler just as long as another routine handles the error. The ability to handle errors in error handlers is fundamental to applying a transaction processing model of error handling to your application, a subject I'll explain further in Tip 14.
To recap, the reason GetAttr doesn't handle many (if any) internal errors is that to do so would take away its very flexibility. If the routine "told" you that the drive didn't exist, by using, say, a message box, you couldn't use it the way we did in bDriveExists.
If you're still not convinced, I'll be saying a little more on why raising errors is better than returning True or False later. But for now, let's think BASICA!
|