View RSS Feed

Development Team Blog

In their own special way, all objects are error handlers

Rate this Entry
Did you know that any object can turned into an error handler object? An application can have one error object at any one time. That object is the object identified in the Error_Object_Id global handle. Anytime an error occurs the Error_Report event is sent to the object Id in Error_Object_Id. If you change the value of Error_Object_id that new value will become the error object and, upon an error, receive the Error_Report event. Thatís all there is to error handling.

Most framework applications use the DFError.pkg. This package creates a class named ErrorSystem, creates an error object based on this class and assigns Error_Object_Id to this object. Once initialized, all errors are directed to this object. If you take a look at the ErrorSystem class you will see that its constructor contains the line
Code:
Move Self to Error_Object_Id
which assigns itself as the error handler. You will also see the Error_Report procedure
Code:
Procedure Error_Report Integer ErrNum Integer Err_Line String ErrMsg
which handles the errors. Thatís what makes this an error handler Ė all of the additional code is fluff.

The frameworkís built in error handler normally does the job just fine. If you wanted, you could create your own replacement for this error handler object. Normally you wonít do this and, if you do, you will most likely base it on a sub-class of ErrorSystem. A more interesting use of error handling is to create a temporary error object, which is used to surround just a few lines of code. Thatís what I want to demonstrate in this article.

We will start by creating a class that performs an Eval() at runtime. One problem with the Eval() function is that it will attempt to evaluate whatever you hand it. If the string can be evaluated (e.g., Eval("100 * 55") ) it returns the evaluated expression. If the string cannot be evaluated (e.g., Eval("John / 55") ) an error is generated. We want to create an eval handler class that traps evaluation errors and handles them within the class. To do this, we will create the class cSafeEval with one public method, Get SafeEval. This function is passed the string to evaluate and returns the evaluated string (by reference) along with a boolean indicating if the evaluation was valid. It would be used like this:
Code:
Get SafeEval of oSafeEval "100 * 55" (&sNewValue) to bOk
If (bOK) Begin
    Send DoSomething sNewValue
End
Here is what the class will look like:
Code:
Class cSafeEval is an cObject
  
    Procedure Construct_Object
        Forward Send Construct_Object
        Property Boolean pbError False
    End_Procedure
  
    Procedure Error_Report Integer ErrNum Integer Err_Line String ErrMsg
        Set pbError to True
    End_Procedure
  
    Function SafeEval String sExp String ByRef sValue Returns Boolean
        Integer hoOldError
        Boolean bError
  
        Set pbError to False // make sure our error flag is false
        Move Error_Object_Id to hoOldError // store the prior error handler
        Move Self to Error_Object_Id  // make this object the error handler
        Move (Eval(sExp)) to sValue
        Move hoOldError to Error_Object_Id // restore the prior error handler
  
        Get pbError to bError
        Function_Return (not(bError))
    End_Function
  
End_Class

Object oSafeEval is a cSafeEval
End_Object
  
Procedure TestEval
    Boolean bOk
    String sNewValue
    Get SafeEval of oSafeEval "100 * 55" (&sNewValue) to bOk
    Get SafeEval of oSafeEval "John / 55" (&sNewValue) to bOk
End_Procedure
  
Send TestEval
We need to trap all errors generated by the Eval() function. We will do this by temporarily making this object its own error handler by reassigning Error_Object_Id. If the Eval() function raises an error, Error_Report will be sent to itself. Rather than reporting the error, Error_Report will set a property, pbError, which will be used by the TestEval function to determine the boolean return value.

Letís make this a little more complicated. We will add a debug mode to this class, which when active, redirects the error to the regular (prior) error handler. I am not quite sure why youíd want to do this, but assume that a good reason exists.
Code:
Class cSafeEval is an cObject
  
    Procedure Construct_Object
        Forward Send Construct_Object
        Property Handle phoOldError 0
        Property Boolean pbError False
        Property Boolean pbDebugMode False
        Property Boolean pbProcessingError False
    End_Procedure
  
    Procedure Error_Report Integer ErrNum Integer Err_Line String ErrMsg
        Boolean bInError bDebugMode
        Handle hoOldError
        Get pbProcessingError to bInError
        If (not(bInError)) Begin
            Set pbProcessingError to True
            Get pbDebugMode to bDebugMode
            If (bDebugMode) Begin
                Get phoOldError to hoOldError
                Send Error_Report of hoOldError  ErrNum Err_Line ErrMsg
            End
            Set pbError to True
            Set pbProcessingError to False
        End
    End_Procedure
  
    Function SafeEval String sExp String ByRef sValue Returns Boolean
        Boolean bError

        Set pbError to False
        Set phoOldError to Error_Object_Id

        Move Self to Error_Object_Id
        Move (Eval(sExp)) to sValue
        Get  phoOldError to Error_Object_Id
  
        Get pbError to bError
        Function_Return (not(bError))
    End_Function
  
End_Class
  
Object oSafeEval is a cSafeEval
    Set pbDebugMode to True
End_Object
  
Procedure TestEval
    Boolean bOk
    String sNewValue
    Get SafeEval of oSafeEval "100 * 55" (&sNewValue) to bOk
    Get SafeEval of oSafeEval "John / 55" (&sNewValue) to bOk
End_Procedure
  
Send TestEval
Notice that SafeEval now stores the value of the prior error handler in the property phoOldError so that Error_Report can use this to redirect the error to that prior error handler. Also notice that weíve added a property, pbProcessingError, which guards against recursive errors. This protects against the case of an error occurring inside of the error handler itself. Since we are sending the error to another object and we donít really know what that object is going to do, this kind of error protection should be considered a requirement.

Being able to redirect errors to the prior error handler is a powerful technique. You could also use this to perform special processing on selected errors (like ignoring them) while passing all other errors to the regular error handler.

Letís create one more example. We want to add a function to an existing object that attempts to open a table. If it fails, we want the function to return false. Specifically we do not want it to close the application.
Code:
Object oOpenFileErrorHandler is a cObject
    Property Boolean piError 0
    Property Boolean pbProcessingError False
  
    Procedure Error_Report Integer ErrNum Integer Err_Line String ErrMsg
        Boolean bInError
        Get pbProcessingError to bInError
        If (not(bInError)) Begin
            Set pbProcessingError to True
            Set piError to ErrNum
            Set pbProcessingError to False
        End
    End_Procedure
End_Object
  
Function SafeOpenTable Handle hTable Returns Boolean
    Boolean bError
    Integer hoOldError hoNewError
    Integer iError
  
    Move Error_Object_Id to hoOldError
    Move oOpenFileErrorHandler to hoNewError 
    Set piError of hoNewError  to 0
  
    Move hoNewError to Error_Object_Id
    Open hTable    
    Move hoOldError to Error_Object_Id
  
    Get piError of hoNewError to iError
    Function_Return (iError=0)
End_Function
 
Procedure Test
    Boolean bOk
    Get SafeOpenTable Customer.File_Number to bOk // should be true
    Get SafeOpenTable 77 to bOk // should be false
End_Procedure
  
Send Test
In this example we didnít use the same object as our error handler Ė instead we created a custom child error object to handle this one function. Using this technique is a little more flexible because you could add additional functions to the main object where each function has its own error handling object with its own unique error handling capabilities.

To cap this off, we will make a generic error trapper class, which can be used dynamically any time you need to trap errors.
Code:
// cErrorTrapperHandler.pkg
Class cErrorTrapperHandler is a cObject
  
    Procedure Construct_Object
        Forward Send Construct_Object
        Property Boolean piError 0
        Property Boolean pbProcessingError False
    End_Procedure
  
    Procedure Error_Report Integer ErrNum Integer Err_Line String ErrMsg
        Boolean bInError
        Get pbProcessingError to bInError
        If (not(bInError)) Begin
            Set pbProcessingError to True
            Set piError to ErrNum
            Set pbProcessingError to False
        End
    End_Procedure
  
End_Class
// end of file
  
// use the error trapper at the top of your file
Use cErrorTrapperHandler.pkg

// the following would be added to an existing object
Function SafeOpenTable Handle hTable Returns Boolean
    Integer iError
    Integer hoOldError hoNewError
  
    // Note that RefClass() is a compiler function added to 15.1
    Get Create (RefClass(cErrorTrapperHandler)) to hoNewError
    Move Error_Object_Id to hoOldError
  
    Move hoNewError to Error_Object_Id
    Open hTable    
    Move hoOldError to Error_Object_Id
  
    Get piError of hoNewError to iError
    Send Destroy of hoNewError
    Function_Return (iError=0)
End_Function
  
Procedure Test
    Boolean bOk
    Get SafeOpenTable Customer.File_Number to bOk
    Get SafeOpenTable 77 to bOk
End_Procedure
  
Send Test
To be honest, I am not sure Iíd every actually bother to create a reusable class for this. You rarely need to create these types of custom error handlers and each need will probably have unique requirements.

Now a few words of warning.

1. If you are going to trap errors in a custom error handler, make sure that this trap is as small as possible. Ideally you are trapping a simple command or a function and not a message that might do all kinds of other things. In all of our examples, we are trapping a single line of code. As a general rule, the more lines of code you are controlling, the more sophisticated your error handler needs to be.

2. Any time you write code like this make sure you thoroughly test it. Test it working and test it failing. This is really important because you are trapping errors and hiding recursive errors. Run this through the debugger. If youíve done something wrong you might never see it in your running program.

3. As you start making your error trapping more sophisticated be very careful. For example, it was tempting to make the error handler class more sophisticated so that it would redirect the error handler when the object was created and release and destroy the error handler when you retrieve the error number. The code to use this would look like this:
Code:
Function SafeOpenTable Handle hTable Returns Boolean
    Integer iError
    Integer hoNewError
  
    // creating the object will create it and redirect the error to itself
    Get Create (RefClass(cErrorTrapperHandler)) to hoNewError
  
    Open hTable    
  
    // releasing the object will restore the prior error handler, return the error code
    // and destroy itself
    Get ReleaseError of hoNewError to iError
    Function_Return (iError=0)
End_Function
If you do something like this be careful. You can make this work, but this might be a classic case of being "too clever for your own good."

4. Be mindful that you are adding code that is being processed when your application is in an error state - you really don't want to do things that might generate more errors. For example, adding additional error handling code to transmit error reports over HTTP might be problematic if the error itself is an HTTP error.

5. Test your code! Yes, this is a duplicate of 2, but it is essential that you run this through the debugger single stepping every single line of code. As I was creating these samples, I made plenty of errors and I would have never found them if I didnít run this carefully through the debugger.

Comments

  1. Allan Greis Eriksen's Avatar
    Good blog. I tend to write an error handler for a whole view or dialog and not just for one line of code. I am convinced that your line-by-line error handling approach is way better and a lot easier to control. Thanks for this tip
  2. Clive Richmond's Avatar
    John why are there two error packages? One for windows, DfError.pkg, and another for WebApp, cWebAppError.pkg.

    A shame the public interface couldn't have remained the same e.g. Trap_Error v TrapError etc. Can’t these two packages be combined into one?
  3. Peter Crook's Avatar
    At the start of my application I check file sizes and resize each as necessary (embedded DB). This requires the files to be opened exclusively but if any of them can't be (e.g. because they're open in DB Explorer) I'll skip them (they can be checked next time the application is run).

    To achieve this I've modified the SafeOpenTable method as follows:
    Code:
    Function SafeOpenTable Handle hTable Integer eMode Returns Boolean
        Boolean bError
        Integer iMode
        Integer hoOldError hoNewError
        Integer iError
      
        Move (If(num_arguments > 1,eMode,DF_SHARE)) to iMode
        Move Error_Object_Id to hoOldError
        Move oOpenFileErrorHandler to hoNewError 
        Set piError of hoNewError  to 0
      
        Move hoNewError to Error_Object_Id
        Open hTable mode iMode
        Move hoOldError to Error_Object_Id
      
        Get piError of hoNewError to iError
        Function_Return (iError=0)
    End_Function    // SafeOpenTable
    so in my size checking routine I can
    Code:
                Get SafeOpenTable iTableNum DF_EXCLUSIVE to bErr
                Get piError of oOpenFileErrorHandler to iErrorNo
                If (iErrorNo = 0 or iErrorNo = 72 ) Begin   // Ignore file if can't be opened exclusively
    The method use to check the file sizes is:
    Code:
    Procedure CheckFileSizes
        Integer iTableNum iNumRecords iMaxRecordsCurrent iMaxRecordsDesired
        Integer iErrorNo
        Handle  hTable
        String  sTable sPhysicalName
        Boolean bSystemTable bResizeFile bErr
    
        Move 0 to iTableNum
    
        Repeat
            Get_Attribute DF_FILE_NEXT_USED of iTableNum to iTableNum
            If (iTableNum > 0 and iTableNum <> 50) Begin
                Move iTableNum to hTable
                Get SafeOpenTable iTableNum DF_EXCLUSIVE to bErr
                Get piError of oOpenFileErrorHandler to iErrorNo
                If (iErrorNo = 0 or iErrorNo = 72 ) Begin   // Ignore file if can't be opened exclusively
                    Get_Attribute DF_FILE_LOGICAL_NAME   of hTable to sTable
                    Get_Attribute DF_FILE_PHYSICAL_NAME  of hTable to sPhysicalName
                    Get_Attribute DF_FILE_IS_SYSTEM_FILE of hTable to bSystemTable
                    Get_Attribute DF_FILE_MAX_RECORDS    of hTable to iMaxRecordsCurrent
                    Get_Attribute DF_FILE_RECORDS_USED   of hTable to iNumRecords
        
                    If (bSystemTable) Move (iNumRecords <> 1) to bResizeFile
                    Else Move (iNumRecords > 0.95 * iMaxRecordsCurrent) to bResizeFile
                    If (bResizeFile) Begin
                         Move (If(bSystemTable,1,iNumRecords * 100/80)) to iMaxRecordsDesired
                         If (iMaxRecordsDesired <> iMaxRecordsCurrent) Begin    // Might be equal for very small file sizes
        
                            Structure_Start hTable "DataFlex"
                                Set_Attribute DF_FILE_MAX_RECORDS of hTable to iMaxRecordsDesired
                            Structure_End hTable
        
                            #IFDEF Testing
                             Showln ('File No.' * String(iTableNum) * '(' - sTable - ')' * 'increased in size from' * String(iMaxRecordsCurrent) * 'to' * String(iMaxRecordsDesired))
                            #ENDIF
                         End
                    End
                End
    
                Close iTableNum
            End
        Until (iTableNum = 0)
    
    End_Procedure   // CheckFileSizes