Exception Handling Dilemma in VFP
February 19, 2006 •
It was brought to my attention a couple of days ago that by switching the Web Connection error management mechanism to a new TRY/CATCH based handler instead of the traditional Error method handler, that there's some loss of functionality.
In Web Connection 4.0 all Process class errors are handled with Error methods on the Process class which basically capture all non-handled errors. The issue with error methods is that you can't easily disable them so in order to have a debug environment where errors are not handled and a runtime environment where they are I had to use a bracketed code approach like this:
*** This blocks out the Error method in debug mode so you can
*** get interactive debugging while running inside of VFP.
#IF !DEBUGMODE
*********************************************************************
FUNCTION Error(nError, cMethod, nLine)
**************************************
LOCAL lcLogString, llOldLogging, lcOldError
*** Have we blown the call stack? If so - get out
IF PROGLEVEL() > 125
RETURN TO PROCESSHIT
ENDIF
nError=IIF(VARTYPE(nError)="N",nError,0)
cMethod=IIF(VARTYPE(cMethod)="C",cMethod,"")
nLine=IIF(VARTYPE(nLine)="N",nLine,0)
*** Make sure we don't bomb out here
lcOldError=ON("ERROR")
ON ERROR *
*** Shut down this request with an error page - SAFE MESSAGE (doesn't rely on any objects)
THIS.ErrorMsg("An error occurred...",;
"This request cannot be served at the moment due to technical difficulties.<P>"+;
"Error Number: "+STR(nError)+"<BR>"+CRLF+;
"Error: "+MESSAGE()+ "<P>"+CRLF+;
"Running Method: "+cMethod+"<BR>"+CRLF+;
"Current Code: "+ MESSAGE(1)+"<BR>"+CRLF+;
"Current Code Line: "+STR(nLine) + "<p>"+;
"Exception Handled by: "+THIS.CLASS+".Error()")
*** Force the file to close and be retrievable by wc.dll/exe
*** NOTE: HTML object does not release here though due to other
*** Object references like Response.
IF TYPE("THIS.oResponse")="O" AND !ISNULL(THIS.oResponse)
THIS.oResponse.DESTROY()
ENDIF
* wait window MESSAGE()+CRLF+MESSAGE(1)+CRLF+"Method: "+cMethod nowait
IF TYPE("THIS.oServer")="O" AND !ISNULL(THIS.oServer)
*** Try to log the error - Force to log!!!
* llOldLogging=THIS.oServer.GetLogToFile()
llOldLogging = THIS.oServer.lLogToFile
lcLogString="Processing Error - "+THIS.oServer.oRequest.GetCurrentUrl()+CRLF+CRLF+;
"<PRE>"+CRLF+;
" Error: "+STR(nError)+CRLF+;
" Message: "+MESSAGE()+CRLF+;
" Code: "+MESSAGE(2)+CRLF+;
" Program: "+cMethod+CRLF+;
" Line No: "+STR(nLine)+CRLF+;
" Client: " + THIS.oRequest.GetIpAddress() + CRLF +;
"Post Buffer: " + THIS.oRequest.cFormVars + CRLF +;
"</PRE>"+CRLF+;
"Exception Handled by: "+THIS.CLASS+".Error()"
THIS.oServer.LogRequest(lcLogString,"Local",0,.T.)
THIS.oServer.SetLogging(llOldLogging)
THIS.SendErrorEmail( "Web Connection Error - " + THIS.oServer.oRequest.GetCurrentUrl(), ;
lcLogString)
ENDIF
ON ERROR &lcOldError
*** Bail out and return to Process Method!
RETURN TO ROUTEREQUEST
ENDFUNC
* EOF wwProcess::Error
#ENDIF
Now this actually worked fine, but it's always been a funky scheme. The main sticking point is that there's a compiler switch requirement to enable and disable the debug mode switch. So it requires a recompile to make the change between the two.
The other more subtle issue is that it relies on RETURN TO to return back to a specified calling method, which is not always reliable. In fact, if anywhere in the call chain an EVAL() execution causes an error the entire error mechanism breaks because VFP 8 and 9 does not support RETURN TO on EVALUATE() calls. When that's the case RETURN TO does a simple return and you end up executing code FOLLOWING the error.
I've never been fond of Error methods in this scenario because there's no deterministic way to return somewhere, so when VFP 8 came out I was glad to see TRY/CATCH and the ability to have more deterministic error handling that allows you to return to a very specific place in your code.
So, with Web Connection 5.0 the Error method and DEBUGMODE approach (which by the way was applied to other classes as well) was replaced with a TRY/CATCH handler around the core processing engine. Here the implementation doesn't have any special error methods but instead it calls into:
*********************************************************************
FUNCTION Process()
************************
LOCAL loException
PRIVATE Response, REQUEST, Server, Session, Process
Process = THIS
Server = THIS.oServer
Request = THIS.oRequest
Response = this.oResponse
Session = this.oSession
Config = this.oConfig
*** Hook method
IF !THIS.OnProcessInit()
RETURN
ENDIF
IF Server.lDebugMode
this.RouteRequest()
ELSE
TRY
this.RouteRequest()
CATCH TO loException
THIS.OnError(loException)
ENDTRY
ENDIF
THIS.OnProcessComplete()
RETURN .T.
ENDFUNC
* EOF wwProcess::Process
The TRY/CATCH captures any errors and then simply calls an overridable hook method that is used to handle the error. A default handler is provided or the developer can override OnError to do whatever is needed. From a design perspective this is much cleaner and doesn't have the potential bug issue using RETURN TO.
But, there's some loss of functionality here unfortunately. TRY/CATCH is nice, but its use ends up with some limitations. It:
- Doesn't give you detailed error information
- You're no longer part of the CallStack – you're back up at the calling level
CATCH allows you receive an Exception object, but unfortunately that object doesn't give a lot of information and the information available in it is not quite as complete as what you get in the error method. The main reason for this is that when a CATCH operation occurs it unwinds the call stack which puts you back of the initial calling method. This means you can't get detailed error information from the call stack level where the error actually occurred. Any PRIVATE or LOCAL variables will be gone, and ASTACKINFO() will only return to you the current call stack which is not the callstack of where the error actually occurred.
This also means your error information is limited to what the Exception object provides and it's not as complete as what LINENO(), PROCEDURE and SYS(16) provide. The result is that in many situations you can't get the LineNo and executing program name the way you can in an Error method, especially at runtime with no debug info in place.
Alternatives? Not really…
So, now I'm trying to figure out some ways around this limitation – unfortunately I don't have a good way of doing that. My first thought was to allow using Error method in addition the TRY/CATCH handler. A couple of thoughts came to my mind:
- Overriding Try/Catch with an Error Method
- Using an Error method in addition to Try/Catch and use THROW to create a custom exception
The first option would allow the developer to create an Error method and do something like this:
FUNCTION Error(lnerror,lcMethod,lnLine)
*** Do your own error handling here
RETURN TO RouteRequest
Unfortunately this doesn't work: VFP doesn't allow RETURN TO or RETRY from within a TRY/CATCH block. Denied.
The second option would be a similar approach but rather than returning capture the error information and then rethrow a custom exception that does contain additional information:
FUNCTION Error(lnerror,lcMethod,lnLine)
this.StandardPage("Error Occurred","Test")
LOCAL loException as Exception
loException = CREATEOBJECT("Exception")
loException.LineNo = lnLine
loException.ErrorNo = lnError
loException.Message = MESsAGE()
THROW loException
ENDFUNC
Unfortunately that also doesn't work the error thrown in the Error method is thrown back up to the next error handler. The only thing that can handle an exception in an Error method is ON ERROR. So that also doesn't work.
Next thought: Take the exception and attach it to a property so any extra error info is available. Then simply return from the Error method. Unfortunately that also doesn't work in that you cannot short circuit the error processing – any code following the original error continues to run.
So in the end it's clear that Error() methods that need to pop up the error chain do not co-exist nicely with TRY/CATCH handlers. I don't see a way that I can make this work using an approach where both are used.
In the end my solution is a workaround by adding an lUseErrorMethodErrorHandling flag to the process class and then based on that skipping the TRY/CATCH call:
IF Server.lDebugMode OR ;
this.lUseErrorMethodErrorHandling
this.RouteRequest()
ELSE
TRY
this.RouteRequest()
CATCH TO loException
THIS.OnError(loException)
ENDTRY
ENDIF
When set error handling works like in the old version of Web Connection, except that the developer is responsible for doing his own error handling. To simulate similar behavior and message formatting as the TRY/CATCH handler you can run code like this:
FUNCTION Error(lnerror,lcMethod,lnLine)
LOCAL loException as Exception
loException = CREATEOBJECT("Exception")
loException.LineNo = lnLine
loException.LineContents = MESSAGE(1)
loException.ErrorNo = lnError
loException.Message = MESSAGE()
loException.Procedure = SYS(16)
this.OnError(loException)
RETURN TO RouteRequest
ENDFUNC
It works, but it's not what I would consider the cleanest implementation. It would have been really cool if VFP would have allowed some option or hook that can be managed when the Exception object is created. Instead of passing a Reference to an Exception you'd pass a type - VFP could instantiate the type and you could override say the Init() of that type to collect any relevant information from the callstack at that point. Or have the reference have a method that gets called on initialization. Or at the very least if the TRY/CATCH supported the ability to THROW out of the error method up the chain.
Alas, this work around does the trick...