Level Extreme platform
Subscription
Corporate profile
Products & Services
Support
Legal
Français
Articles
Search: 

TierAdapter Framework: Handling errors
Ruben Rovira, October 1, 2005
The basic idea is to place, within a Try/Catch block, a call to any process that might generate an exception (is there any process that can't?). When the exception is generated, it will be trapped by the Catch command, and processed differently, depending on the tier where it happens. The tier which...
Summary
The basic idea is to place, within a Try/Catch block, a call to any process that might generate an exception (is there any process that can't?). When the exception is generated, it will be trapped by the Catch command, and processed differently, depending on the tier where it happens. The tier which is closest to the database engine, which is not responsible for the user interface, traps the exception and saves the information into a cursor called cExceptionInformation.
Description

In the previous articles, we gradually got acquainted with the framework classes, and we saw how to create our own classes, based on the classes supplied. In this article, we will see the details of handling and controlling errors, provided by the framework.

Remember that you can get the latest version of TierAdapter and the complete Demo application, including source code, from our framework's main page at SourceForge.

Error handling

Error handling in TierAdapter is done using the Try-Catch structure. For those of you who are not familiar with this method of handling errors, I recommend you read the excellent article written by Antonio Castaño in the UTMag edition from October 2002, where he explains the main features of this structure.

The way to react to an error goes beyond the reach of the framework, therefore, we won't discuss it in the present article, but we want to explain the main tools available for controlling them.

The basic idea is to place, within a Try/Catch block, a call to any process that might generate an exception (is there any process that can't?). When the exception is generated, it will be trapped by the Catch command, and processed differently, depending on the tier where it happens. The tier which is closest to the database engine, which is not responsible for the user interface, traps the exception and saves the information into a cursor called cExceptionInformation. Finally, this cursor is serialized as XML and sent to the tier immediately above it, as a result of the requested process. Let's see an example of how this would work at the data tier level.

Errors in the data tier

Function GetAllEmailAddress() As String

    Local llAlreadyConnected As Boolean 
    Local loMyCA AS rrCursorAdapter

    Try 

        llAlreadyConnected = Not This.ConnectToBackend()

        loMyCA = NewObject( "rrCursorAdapter", ;
                            "rrCursorAdapter.Prg", ;
                            "", ;
                            This.cAccessType, ;
                            This.oADOConnection )
        With loMyCA
            .Alias = "cAllEmails"
            .SelectCmd = "Select cCompanyName, cEmail form Customers"
            .CursorFill()
            .CursorDetach()
        EndWith 

    Catch To loError

        loMyError = NewObject( "rrException", "rrException.prg" )
        loMyError.Save( loError )

    Finally 

        loMyCA = .F.
        Release loMyCA

        If Not llAlreadyConnected 
            This.DisconnectFromBackend()
        EndIf 

    EndTry 

    Return ( This.Serialize( "cAllEmails" ) )

EndFunc 

As an example, this method returns all e-mail addresses from a table called Customers. For this purpose, within the TRY block, it tries to connect to the database engine, and through a CursorAdapter, extract the required data. In case there is an error in any of the operations involved, the exception generated will be caught by the CATCH, into an exception object called loError. Then, within the same CATCH block, an object rrException is created (see rrException.prg), invoking its Save() method, passing, as a parameter, the exception recently trapped. This method is in charge of storing the data about the error in the cursor cExceptionInformation, which was created when the rrException object was created. Following with the FINALLY block, which gets executed whether or not there is an error, we see that the method destroys the CursorAdapter created previously, and connects to the database engine. At the end of the activities of the analized method, we see that it returns the result of the call to method Serialize() that verifies the existence of the cursor cExceptionInformation. If it exists, its content is converted to XML. In case it doesn't exist, what is passed to XML are the data of the cursors whose names have been received as parameters.

Before we go ahead and see how the receiving tier of this XML behaves, let's review the code of some of the methods named in the previous paragraph.

The class rrException

Let's see some of the methods of the class rrException, which, as we can see in the following declaration, is a specialization of Visual FoxPro's native Exception class.

DEFINE CLASS rrException AS Exception

The Init method is responsible for creating the cursor where we are going to save the error information.

Procedure Init
    If Not Used( "cExceptionInformation" )
        Create Cursor cExceptionInformation (;
               Details      Varchar(250),;
               ErrorNo      Integer,;
               ErrorLevel   Varchar(50),;
               LineContents Varchar(250),;
               LineNo       Integer,;
               Message      Varchar(250),;
               Procedure    Varchar(250),;
               StackLevel   Integer,;
               cStackInfo   Memo )
    EndIf 
EndProc 

The Save method is responsible for extracting the information from the exception received as a parameter, and inserts it into the cursor cExceptionInformation.

Procedure Save( toError AS Object ) AS Void
    . . .
    WITH This
        IF VarType( toError ) = "O"
            DO WHILE VarType( toError.UserValue ) = "O"
                toError = toError.UserValue
            EndDo 
            .Details      = toError.Details
            .ErrorNo      = toError.ErrorNo
            .LineContents = toError.LineContents
            .LineNo       = toError.LineNo
            .Message      = toError.Message
            .Procedure    = toError.Procedure
            .StackLevel   = toError.StackLevel
        EndIf 
    EndWith 

    Insert Into cExceptionInformation From Name This
	
    This.LogError()

EndProc

The LogError method is responsible of saving information about the error into a file Log.Err. For this purpose it uses other methods, which you can see in detail in rrException.prg, whose purpose, basically, is to complement the logged information.

Procedure LogError()

    Local lcError AS String

    lcError = Replicate( "*", 80 ) + CR ;
            + [Date: ] + Alltrim( Transform( Date(), "@D" ) ) + CR ;
            + [Time: ] + Alltrim( Time() ) + CR ;
            + [UserName: ] + Alltrim( This.GetUser() ) + CR ;
            + This.GenErrorInfo() + CR ;
            + Replicate( "*", 80 ) + CRLF

    StrToFile( lcError, "Log.err", 1 )

EndProc

The Throw method is responsible for firing the exception.

Procedure Throw()
    Throw This
EndProc

The Serialize method

In this simple method we see how, before serializing, the existence of the cursor containing the error information is checked, and it is then serialized. In case it doesn't exist, the cursors are serialized whose names are provided as method parameters, in a comma-delimited list.

Function Serialize( tcCommaSeparatedCursorList As String ) As String
    Local loMyXA As rrXMLAdapter, lcRetVal As String
    loMyXA = Newobject( "rrXMLAdapter", "rrXMLAdapter.Prg" )
    If Used( "cExceptionInformation" )
        loMyXA.AddTableSchema( "cExceptionInformation" )
    Else
        Local i As Integer
        For i = 1 To Getwordcount( tcCommaSeparatedCursorList, [,] )
            loMyXA.AddTableSchema( Alltrim( ;
                Getwordnum( tcCommaSeparatedCursorList, i, [,] ) ))
        Endfor
    Endif
    loMyXA.PreserveWhiteSpace = .T.
    loMyXA.ToXML( "lcRetVal" )
    loMyXA = .F.
    Release loMyXA
    Return lcRetVal
EndFunc

Errors in the business tier

The first thing the receiving tier does is to de-serialize these data through the GetData method, which, after recognizing the presence of the cursor cExceptionInformation, creates an exception, populates the data with which it extract the cursor, and triggers it automatically, using the Throw command.

Here, the same method returns us the e-mail address from the Customers table, at the level of the business tier.

Function GetAllEmailAddress() As String

    Local lcXML As String

    Try 
        lcXML = This.oEntidad.GetAllEmailAddress()
        This.GetData(lcXML )
        * Más procesos
    Catch To loError
        loMyError = NewObject( "rrException", "rrException.prg" )
        loMyError.Save( loError )
    Finally 
    EndTry 

    Return ( This.Serialize( "cAllEmails" ) )

EndFunc

The XML returned by the data tier is de-serialized by the GetData method. Then, it is processed according to need at this level. The way to trap any error is similar to the previous tier. Let's see the (simplified) code of the GetData method.

Function GetData( tcData As String ) As Boolean

    Local loMyXA As rrXMLAdapter
    loMyXA = Newobject( "rrXMLAdapter", "rrXMLAdapter.Prg" )
    loMyXA.LoadXML( tcData, .F. )
    For Each oTable In loMyXA.Tables
        oTable.ToCursor( .F. )
    Endfor
    loMyXA = .F.

    If Used( "cExceptionInformation" )
        loError = Newobject( "rrException", "rrException.prg" )
        With loError
            .Fill()
            .Throw()
        Endwith
    Endif

    Return .T.

EndFunc

The data received as parameters in XML format are converted to cursors. Then, a verification is done, between the generated cursors, for the existence of our error cursor. In case it exists, an exception is created, it is "filled" with data extracted from this cursor, and it is "thrown" so that it may be trapped by the Catch of method GetAllEmailAddress, which, in turn, traps it and sends the corresponding information to the user tier, following the same method as we have seen in the data tier.

Errors in the user tier

In this simple manner, we "raised" the error from one tier to the next. Repeating this procedure in a chain, when we come to the upper tier, the user should be notified that the exception occurred. This notification should be clear, giving the user a range of options to follow to overcome the difficulty.

In TierAdapter, the generation of information to be given to the user in in charge of the program ParseError.prg. Unfortunately, we have not yet implemented this latter one in a class, so that it can be subclassed and specialized according to the needs of every environment, but I assure you that this is one of the priorities of our extensive "To-Do List".

Let's see the code of our method GetAllEmailAddress in the user tier, and then, how ParseError works to generate the error message.

Function GetAllEmailAddress() As Boolean

    LOCAL loEntidad As Object,;
          llRetVal As Boolean,;
          lcXML As String

    Try 
        lcXML = loEntidad.GetAllEmailAddress()
        This.GetData(lcXML )
        * Más procesos
        llRetVal = .T.
    Catch To loError
        llRetVal = .F.
        Stop( ParseError( loError ) )		
    Finally 
    EndTry 

    Return llRetVal 

EndFunc

ParseError.prg

LOCAL loError AS Object

loError = toException
DO WHILE loError.ErrorNo = 2071 
    * User Thrown Error
    IF VARTYPE( loError.UserValue ) = "O"
        loError = loError.UserValue 
    ELSE
        EXIT
    ENDIF
ENDDO

DO CASE
    Case Between(loError.ErrorNo,9990,9999)	 
        * Error generated by the user.
        Return Alltrim( loError.Message ) + CR ;
               + Alltrim( loError.Details )		
    Case loError.ErrorNo = 1884	
        * The uniqueness of primary or candidate key is violated.
        Local lcField As String 
        lcField = ALLTRIM( loError.Details )
        Return CR ;
              + "Ha intentado ingresar un valor ya existente" + CR ;
              + "en " + UPPER( RIGHT( lcField, LEN( lcField ) ) ) + "." + CR ;
              + CR + "Los datos no han sido guardados."
    OTHERWISE
        Return CR ;
              + [Error: ] + ALLTRIM( Transform(loError.ErrorNo) ) + CR ;
              + [Message: ] + ALLTRIM( loError.Message ) + CR ;
              + [Procedure: ] + ALLTRIM( loError.Procedure ) + CR ;
              + [LineNo: ] + ALLTRIM( Transform( loError.LineNo) ) + CR ; 
              + [Details: ] + ALLTRIM( loError.Details ) + CR ;
              + [StackLevel: ] + ALLTRIM( Transform( loError.StackLevel) ) + CR ;
              + [LineContents: ] + ALLTRIM( loError.LineContents )

EndCase

The initial loop within ParseError searches the original exception that caused the chain of exceptions that finally generated the successive Throws to reach the upper tier. The following DO CASE lets us differentiate the behavior to follow for different types of errors. The general behavior followed by the OTHERWISE takes care of concatenating the error data in a formatted string to be exposed to the user through Stop (see stop.prg).

Conclusion

Today we saw how to control errors that can be generated at different tiers of our application. For lack of space, we will still be in debt, with regards to our promise, in the previous issue, about managing transactions, to which we will dedicate the entire next issue. See you next month.

Ruben Rovira, Desarrollos Independientes
Rubén O. Rovira, (Buenos Aires, Argentina), is a Computer Bachelor. He is devoted to development of custom software for commercial and service companies since 1992. He has used FoxPro and Visual FoxPro as a development tool since version 2.5 (DOS). Nowadays he is an independent Information Systems Consultant, and Lead Developer of the TierAdapter Framework.

Omar Bellio, Soluciones Informáticas
Omar Bellio is a system analyst and independent developer since 1985. He has worked with Fox since its very first version. Today he develops applications for a variety of purposes using Microsoft tools (VFP, VB, .NET PLATFORM, SQL Server, etc.) and also Oracle.
More articles from this author
Ruben Rovira, May 1, 2003
In this article I will talk about ASP.Net mobile controls, and how to use them to puch the functionality of our applications to the domain of mobile devices such as cell phones and PDAs. I will also create a simple web mobile application as an example, using a component developed in VFP8 to sh...
Ruben Rovira, September 1, 2002
In this article we shall see how to use images in forms and reports, in our applications developed with Visual FoxPro. We will also talk about how to optimally organize and save images on disk. Introduction How often have we, as developers in the DOS era, desired to have the possibility t...
Ruben Rovira, June 1, 2005
TierAdapter is an n-tier application development framework developed in Visual FoxPro. Briefly, it implements a hierarchy of classes that makes it possible to quickly develop components for the data access tier, easy to change between the native VFP engine and SQL Server or other engines available t...
Ruben Rovira, July 1, 2005
In this second issue of the series about the framework, we review the Demo application, the way it works and the main features it provides, so in the future issues we can explain how to implement a solution.
Ruben Rovira, September 1, 2005
In this issue, we will continue with the creation of the layers for an entity that includes more than one table, in a header-detail schema; as we shall see, also in this case, the required code is minimal.
Ruben Rovira, August 1, 2005
In this new issue, we will show the steps to follow to develop an application from zero. Also, as mentioned at the end of the previous issue, we will demonstrate that very little code has to be written in order for everything to work correctly.
Ruben Rovira, December 1, 2005
This time, we will briefly make a detour from the transactions (until the next issue), and look at different ways in which the framework allows us to distribute application components. This will serve as a basis, so that we can later analyze how to use transactions in every one of these cases.
Ruben Rovira, November 1, 2005
Any database engine, in order to be worthy of receiving this designation, should offer the possibility of handling transactions. The appropriate handling of transactions (among other things) allows us to guarantee the integrity of the stored data. For those with less experience in this subject, I sh...
Ruben Rovira, April 1, 2003
In the following paragraphs I shall try to summarize what web services are and later, step by step, I'll explain how to create a web service usign VFP8. This web service will be used in the third part of this note where, always from VFP8, we'll develop a small application that will consume the ...