Wednesday, April 05, 2006

Error Handler Gets Harder

One of the best things added to VFP 8 is structured error handling using TRY ... CATCH ... ENDTRY structures. It can also make error handling harder than it used to be.

First, some background. I use a Chain of Responsibility error handling mechanism I discuss in my error handling white paper. All of my base classes have code in their Error methods that implement this mechanism. If an error occurs in an object and that object's Error method doesn't handle it, the error is bubbled up the class hierarchy, then the containership hierarchy. If no object handles the error, the global error handler (stored in an object named oError) handles it. This mechanism has served me well for more than a decade.

One issue with this design, though, is dealing with anticipated errors. It's kind of a pain to put code into the Error method of an object just to deal with things you know could go wrong, such as opening a table. That means having to use code like:

lparameters tnError, tcMethod, tnLine
local lcReturn
do case

* Handle a specific error.

case tnError = SomeValue
* deal with it

* Use the normal error handling mechanism for all other types of errors. Note
* that we add a period to the method name passed. This tells our parent class
* to return the error resolution string back to us, which is required for RETRY
* to work.

otherwise
lcReturn = dodefault(tnError, '.' + tcMethod, tnLine)
* deal with the various return values
endcase
The reason I had to do it this way is because once you have code in the Error method of an object, local error handling like the following is ignored; llError never becomes .T. because the Error method is fired instead.

on error llError = .T.
llError = .F.
* some code that may fail
if llError
* deal with it
endif llError
TRY to the rescue. All of the ugly error-specific code in Error can now be removed and true local error handling implemented:

try
* some code that may fail
catch to loException
* deal with it
endtry
However, now we have a big problem: under some circumstances, we can no longer deal with the error.

Here's the scenario: the global error handler presents a dialog to the user, informing them of the problem and asking if they want to continue working in the application or quit. If they choose to continue, we need a way to prevent execution from returning back to the method that caused the error, since that would almost certainly cause more errors. So, the error handler object has a cReturnTo property that specifies where to RETURN TO when the user chooses that option. cReturnTo is normally set to the name of the method containing the READ EVENTS statement for the application, and the following code does the trick:

lcProgram = This.cReturnTo
do case
case inlist(_vfp.StartMode, 2, 3, 5)
* use COMRETURNERROR
case not llReturnTo
case not empty(lcProgram)
return to &lcProgram
otherwise
return to master
endcase
Normally, this works great. If an unexpected error occurs, the user has the choice of continuing to use the application or quitting. If they choose to continue the application, they usually don't lose any work (such as a new report they were working on), which they would if they chose to quit.

Here's where the problem comes in: if the code in a TRY block calls a method of an object and that object has code in its Error method (which all of my base classes do), the error is handled by the Error method rather than the TRY block. So, we're sort of back to the "ON ERROR gets ignored" scenario, except we're worse off. To see why, imagine code like this:

try
SomeObject.SomeMethod()
catch to loException
* deal with it
endtry

define class SomeObject as SFCustom of SFCtrls.vcx
function SomeMethod
* some line of code causing an error
endfunc
enddefine
Because SomeObject is a subclass of SFCustom, ultimately the error is going to bubble up to the global error object. When the user chooses to continue with the application, the RETURN TO &lcProgram statement executes, and BOOM! The user gets hit with a "RETURN/RETRY statement not allowed in TRY/CATCH" error, and because we're inside an error handler, the built-in VFP error handler kicks in and we can say the application has crashed.

Aha, but we could use the SYS(2410) function to determine whether we're inside a TRY structure and then handle that, right? Wrong -- that function tells you what mechanism is used to handle an error, not whether there's a TRY structure somewhere in the call stack. So, how do we determine whether to allow the user to choose to continue if we don't know whether we're in a TRY or not?

My workaround for this works but feels like a kludge, so I'd appreciate any suggestions to handle this better. I added an lInsideTry property to my global error handler object and if that property is true, the error handler tells the user that "Due to the nature of the error, the application must shut down" and hides the Continue button. An additional CASE statement in the code I showed above quits the application if lInsideTry is .T. lInsideTry is normally .F., but any TRY structure that calls the method of an object now looks like this:

oError.lInsideTry = .T.
try
SomeObject.SomeMethod()
catch to loException
endtry
oError.lInsideTry = .F.
Fortunately, there are only about a half-dozen places in Stonefield Query where I had to resort to this ugly code.

In conclusion, while some new features make things easier, in some cases, mixing new and old features can sometimes have unexpected consequences or even make things harder.

No comments: