Rick Strahl's Weblog
Rick Strahl's FoxPro and Web Connection Weblog
White Papers | Products | Message Board | News |

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...

Posted in:

Feedback for this Weblog Entry