Best Practices Error Handling in Visual FoxPro

Doug Hennig

Error handling in Visual FoxPro is both more flexible and more complex than in FoxPro 2.x. This column examines how error handling in Visual FoxPro differs from error handling in FoxPro 2.x and looks at some strategies for creating error handlers. Next month's column will present an application-wide error handling class.

Welcome to "Best Practices," a monthly column that discusses solid programming practices, with emphasis on techniques and ideas specific to FoxPro. Last month I discussed parameters and ways to make the best use of them. This month, I'll discuss error handling in Visual FoxPro.Like a lot of things in VFP, error handling is both more flexible and more complex than it was in FoxPro 2.x. There's a new function to help determine why an error occurred and a new error trapping mechanism that supplements the ON ERROR trapping you're accustomed to using. However, from FoxPro 2.x the number of possible errors has almost doubled to more than 600 in VFP. Also, under certain conditions, some errors get trapped automatically while others can't be trapped at all. Because error handling in VFP is a large topic, I'll examine the best practices for handling errors in two columns. This month, I'll look at how VFP differs from error handling in FoxPro 2.x and discuss some strategies for creating a global error handling routine. In next month's column, we'll create the framework of an application-wide error handler.

Error handling basics

There are several aspects involved in error handling: setting up the error handler, determining the cause of the error, informing the user what happened (and possibly logging it to an error file), and trying to resolve the problem (trying to execute the command again, returning to the statement following the one that caused the error, quitting the application, and so on).

Setting up a global error handler in VFP is no different than in FoxPro 2.x: you still use ON ERROR, like this:


 on error do ERR_PROC with error(), sys(16), lineno()

The parameters tell the handler the error number, the name of the program, and the line number of the code executing when the error occurred. You can pass additional parameters to the error handler if you wish.

Several functions in FoxPro 2.x and VFP help determine the cause of an error, including ERROR(), MESSAGE(), LINENO(), SYS(16), and SYS(2018). VFP also provides a new AERROR() function, which places information about the most recent error into an array (see the VFP documentation for details about this function). Although some of the information in the array can be obtained from other functions, for some types of errors (such as OLE, ODBC, and triggers), AERROR() provides information not available elsewhere (such as which trigger caused the error).

Informing the user that an error occurred is fairly straightforward, and is often combined with allowing the user to determine how the problem should be resolved. The main concerns here are deciding what to tell the user and what choices to present. The message to display will vary with the type of error and should be worded in a calm tone to prevent the user from panicking and doing a "three-finger salute." It could even provide information about how to resolve the problem. For example, something simple like an off-line printer can be handled by asking the user to make sure that paper is properly loaded in the printer, that it's turned on and connected to the computer, and so forth. The user can be given the choice of trying to print again or canceling the print job. If a user tries to edit a record that's locked by another user, you might tell the user that someone else is editing the record right now, and give them the choices of trying again or canceling the edit.

Given that there are more than 600 possible errors in VFP, the thought of coming up with meaningful messages for all those errors might horrify you. Fortunately, if you look at the VFP help topic on error messages, you'll see that most of them fall into one of a few categories:

One way to deal with this limited diversity is to create a table containing error numbers and the message to display. Your error handler would look up the error number that occurred in this table and display the appropriate message. Errors falling in the same category would have the same message. For example, any of the errors indicating that the system is out of memory (error numbers 21, 43, 1149, 1150, 1151, 1600, or 1986) would have a message telling the user that memory resources are low and suggesting they shut down all open applications before trying again.

"That doesn't sound too bad," you're probably thinking, "but who wants to create that table for more than 600 error numbers?" Fortunately, VFP has a new ERROR command that causes an error to occur. You specify the error number (or message, which allows you to create user-defined errors), and VFP thinks the specified error has occurred. This command is a fabulous way to test your error handler because it allows you to easily force a specific error to occur and see how your routine handles it, rather than trying to come up with a condition that would cause the error to occur. A fast way to create a table of error numbers is to create a program that generates every possible error number and create a record in a table with the error message that results from each error. Here's a program that does just that (this program, which is called MAKEERR.PRG, is on the Developer's Disk):


 create table Errors (ErrorNum I, Message M)
index on ErrorNum tag ErrorNum
on error do SaveErr with error(), message()
for lnI = 1 to 2000
    error lnI
next lnI
on error
use

procedure SaveErr
lparameters tnError, tcMessage
if tnError <> 1941  && ignore "invalid error code" errors
    insert into Errors values (tnError, tcMessage)
endif tnError <> 1941
return

Before displaying the error message to the user, I like to log the error to an error log file. This has proven to be invaluable time and again, because when users report errors they're frequently too vague about the specifics to do you much good.. The error log file can be a text file, but I prefer a table with fields for the date and time of the error (this can be a DateTime field in VFP), name of the user, error number and message, line number and code where the error occurred, and a memo field containing the current contents of the memory variables (using SAVE TO MEMO <memo name>).

I also include a field called ACTION that contains what action the user took when the error message was displayed. Why would you care about this? Well, occasionally you'll find users who, in response to an error message, reboot the computer, which can cause even more problems than the original error. In order to determine who's been rebooting their systems, I put "REBOOT" into the ACTION field before displaying the error message, then allow the user to indicate what action they'd like to take, and put their choice into ACTION. If the user reboots rather than selecting a choice, "REBOOT" still appears in the ACTION field. When I check the error log file, I look for those users who've rebooted their systems and have a little talk with them about the dangers of such practices.

Resolving an error can be complicated. RETRY tries to re-execute the command that caused the error, but with most errors this doesn't help since most users have limited ability to resolve the problem themselves. RETURN continues executing from the line following the one that caused the error, but since the command that caused the error in effect didn't execute, frequently a second error will occur because something didn't happen, such as a variable not being created. QUIT is a valid option, since many errors result from a programmer or system error, and there's not much point in carrying on until the problem is resolved. As a developer, you'd also like an option to CANCEL back to the Command window. The code doing that should clean up the environment as much as possible.

Another option is to use RETURN TO MASTER or RETURN TO <program> to exit the program that caused the error and go back to the main program or some other specific program. You need to be careful with this option, since it might leave the system in a messy state: forms and windows will still exist, tables or cursors will still be open, and so on. It's a good idea to clean such things up as much as possible before using the RETURN TO command. This means the error handler will need some knowledge of the structure of the application. For example, in the error handler I created for Stonefield AppMaker (an application development framework tool), the following pseudo-code is executed when the user chooses to exit the current program but stay in the application (the actual code isn't shown only because many of its details aren't important for this discussion):

Object.Error and Form.Error

The type of error handling I've been discussing so far is global in scope; in other words, a single routine is used to handle all errors in the application. However, VFP has supplemented the global ON ERROR handler by allowing you to provide individual local handlers by implementing the Error event.

Every object in the Visual FoxPro event model has an Error event. Of course, not every object will have an Error method. If this distinction isn't clear to you, remember that an event is an action triggered by something the user or the system does (such as a keystroke, a mouse click, or something that Visual FoxPro thinks is an error), while a method is the code that executes when the event occurs. The code for a method will also execute when a message is passed to an object telling it to execute that method. With many events, such as a mouse click, if the object doesn't have any code for the appropriate method, the event is ignored. However, when an error occurs, what happens depends on a number of things.

The Error method of an object will be called if it exists and an error occurs in a method for the object. The form ERROR1 on the Developer's Disk shows an example of this; the command button has an error in the Click method, but it has an Error method, so the error can be trapped. The Error method of an object isn't called if an error is caused by interactive changes the user makes. For example, if you have a text box bound to the PRICE field in a table, and the validation rule for PRICE is BETWEEN(PRICE, 0, 10), entering 20 into the text box won't cause the text box's Error method to execute. Instead, VFP displays the validation text for the field or a generic error message if there is no validation text. The form ERROR2 on the Developer's Disk shows an example of this.

What happens if there's an error in a method of an object but the object doesn't have an Error method? For example, suppose the code in the Click method of a command button has a syntax error, but the button doesn't have an Error method. When I first started playing with error handling in Visual FoxPro, I assumed the Error method of the parent for the object (such as the form) would handle the error. However, this isn't the case. Instead, the ON ERROR routine (if there is one) is called. If there is no ON ERROR routine, Visual FoxPro does its own error handling, and we consider that the application has crashed. Thus, a form's Error method doesn't act as a form-level error handler (that is, an error handler for all objects in the form lacking their own Error method), but is only used when an error occurs in a method of the form itself. The form ERROR3 on the Developer's Disk demonstrates this.

Why would you want a form-level error handler? One reason is to consolidate all of the error handling logic in one place rather than duplicating the code in the Error method of every object (or at least every different class you're using). Another is to provide three (or more) layers of error handling. An object could have an Error method handling errors specific to its needs and delegating the rest to the form's Error method; any object not needing to handle specific errors wouldn't have an Error method at all, so the form's Error method would be called. The form's Error method would handle any errors specific to the form and delegate the rest to the application-level error handler. Thus, error handling would become less specific and more generic as you move up the error "tree." Unfortunately, without coding for it, this type of error handling doesn't occur. You'll see how to implement such a mechanism in a moment.

Since the form's Error method doesn't act as a form-level error handler by itself, my first attempt to create a form-level error handler was to use ON ERROR Thisform.Error(ERROR(), PROGRAM(), LINENO()) in the Init method of the form. I thought this would result in the form's Error method being called whenever an object didn't have its own Error method; an error in that case would trigger the ON ERROR routine, which points to the form's Error method. However, this doesn't work either; Visual FoxPro returns a "THISFORM can only be used in a method" error message. This is because when an error occurs, Visual FoxPro executes code (the command following ON ERROR) that's outside the scope of an object, and the name "Thisform" can only be used in a method of an object in the form. The form ERROR4 on the Developer's Disk demonstrates this. However, something that does work is to use ON ERROR _screen.ActiveForm.Error(ERROR(), PROGRAM(), LINENO()). This works because _screen.ActiveForm is always in scope (unless there's no active form), even when called from an error event. The form ERROR5 on the disk shows how this works.

Here's a summary of how errors in objects are handled:

Designing an error handling scheme

Let's look at one possible design of an error scheme for an application. You want to handle errors in the most efficient manner possible, yet still provide the ability of individual objects to handle their own specific errors. This scheme implements the three-layered approach I mentioned earlier.

Object

Unless an object needs to handle specific errors, it won't have an Error method. If it does need to handle specific errors, its Error method should handle them and delegate any errors it doesn't handle to the ON ERROR handler. The following code is an example. In this code, the STRTRAN statements substitute the name of the method that caused the error for SYS(16), the error number for ERROR(), and the line number for LINENO(). This ensures that the ON ERROR handler is called with the proper values from the routine that caused the error, not this routine. Here's the Error method:


 LPARAMETERS nError, cMethod, nLine
local lcError
do case
    case nError = <error we can handle>
        * handle it
    otherwise
        lcError = strtran(on('ERROR'), 'sys(16)', ;
            "'PROCEDURE " + upper(This.Name + '.' + cMethod))
        lcError = strtran(lcError, 'error()', 'nError')
        lcError = strtran(lcError, 'lineno()', 'nLine')
        &lcError
endcase

Form

As with objects, unless a form needs to handle specific errors, it won't have an Error method. If it does need to handle specific errors, the form's Activate method could save the current ON ERROR setting in a property of the form and point ON ERROR to the form's Error method. The form's Error method would handle specific errors and delegate any errors it doesn't handle to the saved ON ERROR handler. It would be executed not only when errors occurred in its own methods, but when any object on the form doesn't have its own Error method or when errors are delegated from an object to the ON ERROR handler. The Deactivate method of the form would restore the saved ON ERROR handler.

Here's an example of the form's Activate method:


 This.cCurrOnError = on('ERROR')
on error _screen.Activeform.Error(error(), ;
  sys(16), lineno())

Here's an example of the form's Deactivate method:


 local lcError
lcError = This.cCurrOnError
on error &lcError

Here's an example of the form's Error method:


 LPARAMETERS nError, cMethod, nLine
local lcError
do case
    case nError = <error we can handle>
        * handle it
    otherwise
        lcError = strtran(This.cCurrOnError, 'sys(16)', ;
            "'PROCEDURE " + upper(This.Name + '.' + cMethod))
        lcError = strtran(lcError, 'error()', 'nError')
        lcError = strtran(lcError, 'lineno()', 'nLine')
        &lcError
endcase

ON ERROR

The ON ERROR handler should be set up at application startup. The error routine should accept the same parameters as an Error method: the error number, the method or program causing the error, and the line number where the error occurred. It should use the AERROR() function to get any other error information it needs. The following code is an example at application startup:


 on error do ERR_PROC with error(), sys(16), lineno()

Here's the accompanying Error handler:


 procedure ERR_PROC
LPARAMETERS nError, cMethod, nLine
local laError
= aerror(laError)
* handle the error

Conclusion

You've seen that while VFP gives you more options for handling errors than FoxPro 2.x, that flexibility comes at the price of more complexity. In next month's issue, we'll look at an application-wide error handler implemented as a class. The code won't be a complete, ready-to-use routine, but it will be a framework you can flesh out to provide error handling services to your applications.

Doug Hennig is a partner with Stonefield Systems Group Inc. in Regina, Saskatchewan, Canada. He is the author of Stonefield's add-on tools for FoxPro developers, including Stonefield Data Dictionary for FoxPro 2.x and Stonefield Database Toolkit for Visual FoxPro. He is also the author of The Visual FoxPro Data Dictionary in Pinnacle Publishing's "The Pros Talk Visual FoxPro" series. Doug has spoken at user groups and regional conferences all over North America. CompuServe 75156,2326.

To find out more about FoxTalk and Pinnacle Publishing, visit their website at
http://www.pinpub.com/foxtalk/

Note: This is not a Microsoft Corporation website.
Microsoft is not responsible for its content.

This article is reproduced from the June 1996 issue of FoxTalk. Copyright 1996, by Pinnacle Publishing, Inc., unless otherwise noted. All rights are reserved. FoxTalk is an independently produced publication of Pinnacle Publishing, Inc. No part of this article may be used or reproduced in any fashion (except in brief quotations used in critical articles and reviews) without prior consent of Pinnacle Publishing, Inc. To contact Pinnacle Publishing, Inc., please call (800)788-1900 or (206)251-1900.