David Bayliss On The ErrorClass

by David Bayliss

Published 1999-02-22    Printer-friendly version

Who can understand his errors?
Cleanse thou me from secret faults. Ps 19:12

In Psalm 19 David meditates on the judgements of God and the way that suitable correction can keep the servant profitable. But then he raises the important question, do we always understand the nature of the error? Does the method of correction give us enough information to avoid repetition of the problem? Are we prepared to have the error corrected or would we rather continue as we are? It raises a second issue, the undetected error. Whilst we might happily continue, confidant in our own minds that we are perfect, it is quite possible that faults and defects lie under the surface that are detracting from our overall performance.

Now David was clearly considering issues more fundamental and personal than a computer program but in many ways the construction of the ErrorClass has to reflect many of the concerns expressed in our opening verse. For the ErrorClass to be invoked it is probably the case that a condition or defect has been detected outside of the will of the creator of the program (you). It is when the chips are down that real quality shines. A chain is only as strong as it's weakest link. As such the ErrorClass is one of the most fundamental and basic of the ABC classes, it is the one written first and it is at the bottom of the pile (it doesn't use other classes).

Aim

Consider the external (developer) requirements placed upon it :

  1. It must be able to function in a hostile environment (it will be called because other parts of the ABC system are sick). This means it can't be too flash (showy/gaudy) and should assume the minimum.
  2. It should be able to interact with the end-user when required.
  3. It should quickly and cleanly execute any corrective action required
  4. The developer should be able to add or tailor any error message with a minimum of fuss (especially 'foreign' languages <such as American!>)
  5. The developer should be able to tailor the corrective action taken
  6. The developer should be able to tailor the error screens presented.
  7. It should be easy to throw an error message.
  8. It should allow the separation of error recording and reporting when required. This one needs explaining. Consider an ABC method that is performing some action inside a 'Try' style method ( eg FileClass.TryFetch ). It encounters some error that it wants to register with the error class. It then returns to its' caller the error flag. The caller then needs to be able to find out what kind of error happened, and in some cases cause the user to be informed. Now the caller cannot just re-raise the error (as the global error state will have changed). So the error class needs to be controllable with regard to which errors it records and which it doesn't. Because it is possible for errors to be recorded yet not externally detected many of the other ABC methods return an error state. This is particularly noticeable within the FileManager class.


Now on top of the actual requirements placed upon the class when written, there is a hidden agenda. Put another way, I like to keep my options as open as possible for as long as possible. There were three extra issues I wanted taken into account :

  1. Parts of the CW library may wish to use error managers without 'corrupting' the error manager seen by the user. Therefore although the templates will probably have a global error manager, the base classes shouldn't assume it.
  2. The global error reporting variables (ERROR() etc) are really getting long in the tooth in an environment where multiple files are being accessed. Future file drivers may want to report back error conditions using more modern methods (such as the BUILD command sending 'building' events). It would be nice to write the fileclass in such a way that the library can move forward without breaking the developers programs.
  3. I believe exceptions should be the exception. I don't want every third line of code reading IF Something_Horrible_Happened THEN DoThis .

Design Considerations

The aims are always there to depress. When reading that kind of spec, I look out for repeated words, especially ominous ones. Looking at the above the word is 'tailor'. It means everything has to be soft. No quick hacks or hard-coding. We are writing an engine, not a solution.

The next thing to consider is 'what is on my side'.

Number one is that although the ErrorClass must be light (not use too many resources when dormant) it does not have to be that efficient (it being invoked will typically result in user interaction). Further there will probably not be that many of them floating around so memory consumption is not a horribly big issue.

The next thing to consider is who is going to be use the various parts of the specification and how. It is the insight at this point that leads to the design of the class. If you look at the above the class falls into two very distinct usage camps, you and me. More accurately, 1, 7, 8, a, b & c are mostly the concern of people writing ABC classes (or extensions). 3,4,5,6 (possibly 7, although templates help here) are the concern of the application employers. 2 is the concern of the end user, any problems here I can blame someone else for because of 3,4,5 & 6. This duality of problem means we may split the method of solution. This may or may not help, but at this stage we're looking for anything that may help.

Also you need to consider what level of developer will wish to make use of this functionality. Is it some baroque tit-bit to satisfy the bit heads, or day by day functionality to be used by everyone. In this case 3,4,5 definitely fall into the latter camp. This means we need a no-coding solution. The part of the solution for 'ABC' coders can be tougher if needs be.

Another plus is that most people will be content to do 3-6 at compile time so we can make that a restriction of the easy way, run-time tailoring can call for more work.

The Angle

In any given class, I like to have a unifying theme or concept that makes the class a class (as opposed to a collection of classes). In this case the whole class hangs around the .trn file. I will explain the structure of this file presently, however the notion is simply this. Define all errors and actions in a data item (or structure). This structure can clearly be altered and therefore all the required tailoring can be done at run-time (code tends to need compile time modifications). The defaults for this structure can be stored in a simple ascii file (the .trn) which can be safely tackled by any developer. Further the .trn is independent of both the class implementation and interface and can therefore remain constant throughout future releases. You may almost think of the .trn as a program (in a peculiar language) that is interpreted by the error class. Hey! Interpretation is slow! Yup, my design considerations say that is ok.

So what data actually needs storing for each error? We choose four :

  1. An id to identify the error (these are defined in aberror.inc so they are available wherever the error class is used).
  2. A severity level. The aim of this is to give a 'first stab' at the required corrective action. The values cover 'do nothing' (Level:Benign), through 'close the app' (Level:Fatal). In between there are options to question the user for a response (Level:User)
  3. The next field is the title to be used on the user interaction dialog
  4. The final field is the text to be used on the user screen. This field has some magic, namely it can contain macro (or substitution) values. These are detailed in our manuals.

Now I eventually came up with a clever method of storing & using this data, it is detailed under the AddErrors(ErrorBlock) method below.

The Implementation

Probably the way to describe the implementation of the class is simply to talk through the various methods. I am using the C5 sources. There are two obvious orders to use, either alpha or the order in the source file. I'm going to use a third, the order they build upon each other. I assume you have these sources to hand and will not repeat information contained therein.

ErrorClass.AddErrors PROCEDURE(ErrorBlock ErrsIn)

This method is really the heart of our implementation and trn file methodology. In order to work upon our data list we eventually want it stored in some easy data format (we choose a queue). We don't really want the user of the class to have to pre-fill the Q (or even know it exists) so the logical thing to do is have an AddError method that takes the four data types as parameters. The problem is this makes the code to initialize the error manager long and tedious. It also means that to alter how the ErrorManager works would require altering a code file. Further our executable would end up with two copies of the data (one to initialise, one stored). It would also mean imposing an upper limit on the length of an error message.

About Three O'Clock in the morning I had a brain-wave that later became central to the way ABC handles constants (embodied in the ConstantClass). It essentially uses a low-level detail of Pstrings, that the length of the string can be intuited from the data. Using this fact, and known details of the layout of bytes and shorts, it is possible to step through a constant group at a 'byte by byte' level and select out the data items.

This method therefore does precisely that, it contains an extra trick that prevents the strings being duplicated (it is this trick that stops us re-using the constant class). Rather than copy the strings into the queue, we have string references in the queue and simply assign them to the string slice that already exists within the group. This drastically cuts down memory consumption and removes any length limitation imposed by the queue (the pstring is limited to 255 characters of course).

NB: There is one hidden gotcha though. It means that if you do an AddErrors call from within a procedure using a group that is local data you must have the static attribute on the group if that error id is going to be used outside of that procedure (ie once that procedure scope has gone).

ErrorClass.SetId PROCEDURE(USHORT Id,UNSIGNED StartPos)

This method implements the next key feature, finding the correct data record for a given message Id. This is not just a queue get because we want to allow scoping. What I mean by that is we want to be able to alter the behaviour of a given error message within a limited piece of the program execution. We do this simply by saying that AddErrors always appends and that SetId always searches backwards to find the matching Id. The two parameter form is to allow the search to start in mid-list (used by the %previous macro). The one parameter (more common) form does a search from the end of the list.

The Takes

The Take methods are used at the point some form of 'user interaction' is required. TakeError is really the despatcher, it simply calls one of the other Take methods that correspond to the differing error levels available. Now when embarking upon a group of methods, such as the other Take methods my mind automatically does a form of 'parent-hunting' or 'common denominator finding'. If you have to write 5 similar methods, find out why they are similar and common up the code.

Each take method is responsible for taking the data for its' error message, substituting the macros with values from the class' data and then displaying the results and acting upon the user response. The 'action' is dependent on error level, the substitution isn't, so a SubString method was created to handle the common work. The Take methods then become fairly trivial. Message was used in preference to a window to comply with requirement 1.

There is one 'extra' take method, TakeOther, this is really there for internal / 3rd party usage. Essentially it allows for new error levels to be defined which will then be re-routed to this method. It corresponds to the ELSE clause of the TakeError method.

Requirement 6 is met by making each Take method virtual. TakeError does not need to be virtual as it is not called from 'underneath' only from on top.

ErrorClass.SubsString PROCEDURE

This procedure performs the mapping from a string with macros to a string with the macros substituted. This allows all the private data inside the class to be code at, but via an interface. This satisfies requirement b. If errorcode et al begin to disappear Substring will become more complex, but user code is preserved. The beginnings of this are happening already as ErrorText is replaced with FileError rather than Error for an errorcode 90. For reasons of efficiency, the SubString method assumes the Id has already been selected using SetId

Note too the %Previous macro, this allows the user to append a message to the one generated by the system. Up to C5EE these macros have been case sensitive, we will probably change this for a future release.

ErrorClass.SetErrors PROCEDURE

SetErrors is the main routine to implement requirement 8. If the Takes report the errors, then the SetErrors records them. Note too SetFile & SetMessage, these are simply providing additional information that the error class might find useful. At first sight the might seem an overly wide interface, why not set all the 'error state' in one shot? This is really a concession to usage. This form of separation allows, for example, the file class to 'pre-stuff' information into the error handler that may or may not be used later. Thus the FileClass SetThread routine (called at the start of every file class action) does a SetFile. This avoids duplication of information and helps requirement c.

ErrorClass.Throw PROCEDURE(SHORT Id)

At this point we have implemented most of the initial requirements, some extra methods were added to make the interface simpler to use (especially with regard to requirement c). The Throw procedure really undoes requirement 8 to fulfil requirement 7. If you like we are now playing the numbers game. First you implement the methods that give you the functionality, then you try to find the way that interface is used 80%+ of the time, and simplify it. In this case it turns out that quite often you want the error processed immediately, throw does this. We also found that SetMessage happened just before the throw quite frequently, hence ThrowMessage.

ErrorClass.SetFatality PROCEDURE(SHORT Id,BYTE level)

SetFatality is a short cut (you can do the same with scoped AddErrors) to allow the action taken on certain errors to be ignored (or escalated). Thus if you wished to turn off the 'NoRecords' message around a report you can simply do

GlobalErrors.SetFatality(Msg:NoRecords,Level:Benign)

In the opening embed of the report. (You need to reset the fatality level unless you want the change to be permanent).

ErrorClass.RemoveErrors PROCEDURE(ErrorBlock ErrsIn)

I left RemoveErrors to last because it in one of those rare instances of a method that is completely mis-coded and mis-designed purely to make using it easier. Logically RemoveErrors should work on a list of Ids, or an individual Id. The only information you need to remove an error is the index of it. However, the time you use RemoveErrors it will almost certainly be paired with an AddErrors call, you are effectively defining an error scope. In fact the AddErrors will nearly always be at the head of the procedure, the RemoveErrors at the end. Now, if you have an AddErrors you want to make the RemoveErrors match 'perfectly'. So having two different data streams is dangerous. Further the ErrorBlock used by AddErrors is still available (remember it has to be static) so the easiest thing to pass in is the same ErrorBlock again. Thus if you look at the implementation you will find most of the procedure is simply skipping over the information it doesn't want, finding the Id, and then deleting that field.


David Bayliss is a Systems Architect for The TopSpeed Development Center. He has worked upon TopSpeed's compiler and was the chief architect of the Application Builder Classes.

Printer-friendly version

 
 

Search

 

Advanced Search
Topical Index

Related Articles

Subscribe to
ClarionMag

One year: $189

(includes all back issues since '99)

Renewals from $139

Two years: $289

Renewals from $239

More Info

Subscribe Now!

ClarionMag Blog

RSS Feeds

Updates via Email

Enter your Email


Powered by FeedBlitz

Quick Links