Programmers don’t like errors. We’d like to write our code without worrying about all of the terrible things that can happen: bad parameters, uninitialized variables and pointers, division by zero, resource limits, and so on. These are things that can go wrong and can quickly dominate our code, overwhelming the clear expression of the problem and complicating both development and maintenance.
Nevertheless, errors can happen, and what goes wrong at run-time is going to crash your system, cripple your program, damage your database, or, at the very least, raise your support costs.
For most of us, handling errors is a matter of prevention put in place after realizing an error can occur. A large percentage of the deeply nested if statements in the code are dedicated to preventing errors from occurring. Some are placed in advance, and some are placed after testing reveals a weak spot. But there are also if statements which represent true alternative paths through the code, required by the logic and design. Distinguishing between that type of code and that which protects against errors can be difficult, and can make maintenance more difficult. For instance:
if (myIndexDef != NULL)
{
for (int I=0;I<myIndexDef->Count;I++)
{
delete (TIndexDef *)
myIndexDef->Items[I];
};
myIndexDef->Clear();
};
Especially troublesome in well-structured programs is dealing with errors that originate in one class or method that can have an effect on another. Programmers have evolved a variety of strategies including status variables and return codes. But just like the if statements, these obscure a program’s structure. Further, the require every participant in calls down through layers of methods to understand and participate in error handling, even if they are unaffected by the error. For instance:
if (DoSomething(Parameter) < 0)
return -1;
// Pass on error from DoSomething
Inevitably some error information is lost, if only information about where the error happened, and what it meant when it happened there. For instance, in the above example, DoSomething() may return a variety of negative error codes, but the routine which detects the error converts them all to -1, which gets passed to the next level up, and which, consequently, knows less about the error than may be necessary to provide a useful error report.
Ideally, errors and decisions could be separated into completely different types of code, each with their own characteristics, each grouped together. To some extent, that is the purpose of C++ exception handling. Observe the following conversion of our first example to an exception-based framework:
try
{
for (int I=0;I<myIndexDef->Count;I++)
{
delete (TIndexDef *)
myIndexDef->Items[I];
};
myIndexDef->Clear();
}
// This exception occurs when
// a pointer is invalid
catch (EAccessViolation &AccessViolation)
{
// Don't do any of the above if an
// exception occurs because myIndexDef
// is NULL
};
The try keyword introduces a block of code that is subject to exception handling. The closing catch identifies the class of exception that is to be caught. Note that exceptions are rooted in the Exception class. There are many built-in classes of exception and you can also create your own. The catch clause will catch every exception of the stated class, and any descendants of that class, so when you are catching multiple, related exception classes, make sure the most specific one is first. For instance, when catching database errors, EDBEngineError is a descendant of EDatabaseError. Therefore, if you want different handling for each of those exception classes, you must specify EDBEngineError first, then EDatabaseError. For example:
try
{
Something();
}
catch (EDBEngineError &DBEngineError)
{
HandleEngineError(DBEngineError);
}
catch (EDatabaseError &OtherDbError)
{
HandleOtherDatabaseError(OtherDbError);
};
As you can see, the try/catch localizes the handling of exceptions to the end of the code concerned. A special type of statement, easily identifiable, is used to handle exceptions. All in all, this is an improvement over the use of if statements to prevent errors, at least in terms of providing clear code. I should also point out that providing three dots in a catch statement would catch all exceptions, regardless of the type. For example:
try
{
Something();
}
catch (...)
{
// all exception are caught
}
An exception is, well... an exceptional event. Generally speaking, exceptions are used to handle operating system and API level errors. But they can be useful for much more.
Nevertheless, there is still a place for error detection before the fact. Indeed, it can work with exception handling to produce a much more robust and comprehensive error handling system.
One of the reasons for catching exceptions in C++ is to manage memory, mostly to make sure memory is released in the event of an error. Fortunately, C++ Builder offers an extension to the exception handling system, which makes this even easier.
Derived from Delphi, the __finally statement allows you to create an exception handling block where the logic in the __finally clause is executed whether or not an exception occurs, and regardless of where in the try block the exception happens. A typical pattern might be as follows:
TStringList *StringList = new TStringList;
try
{
DoSomething();
DoSomethingElse();
}
__finally
{
delete StringList;
};
Regardless of what happens in the try block, the __finally block is always executed. The result in this case is that the string list is always deleted and its memory is always reclaimed.
One apparent disadvantage of the try/__finally combination is that __finally cannot be mixed with catch. However, this is not actually a disadvantage, since it requires separation of memory management in the face of an error from handling the error. Extending the above example to handle errors as well as release memory is simple:
try
{
try
{
DoSomething();
DoSomethingElse();
}
__finally
{
delete StringList;
};
}
catch (EDBEngineError &DBEngineError)
{
HandleEngineError(DBEngineError);
}
catch (EDatabaseError &OtherDbError)
{
HandleOtherDatabaseError(OtherDbError);
};
In the event of an exception, the __finally clause performs its logic and then passes the exception on. In this way the exception is capable of being caught by the outer try block, and the memory is still freed.
Most programmers who use exception handling use it at the local level; preventing errors from propagating out of a method or class, or preventing the premature termination of a loop when one iteration’s operation fails.
But every program needs a more cohesive approach to errors than simple local error trapping. The typical program consists of several levels, each of which has its own needs for error handing and for passing error information to higher layers. Those layers are:
Hardware and operating system
VCL and third party components
Generic components developed at your installation
Application-specific components developed at your installation
Application data modules
Application user interfaces
Of these levels, only level 6 can safely and reliably decide how to report errors and exceptions to the users. Therefore, the key focus of error management must be to promote errors up to level 6, or to at least up to where they can be logged to databases or files at level 5. However, it is best to let level 6 decide to use the resources of level 5 for logging as needed.
From a support perspective, errors must be managed so that when a user reports an error to support, the support personnel can quickly determine the cause, location, and context of the error. This can be managed by a simple strategy.
It is important to remember that the meaning of an error at level 3, the first level over which you have any control, is quite different from the meaning of the error at level 6. At level 3, the error may be EListError, but at level 4 it is “Attempt to insert a service type in the service type list failed”, at level 5 it is “Update of Package record failed”, and at level 6 it is “Unable to ship package.” To the user, the level 6 perspective is the most relevant, and it is usually the message which is displayed. The problem is that the support person needs to know more, and the programmer needs to know even more to fix the problem.
The solution to this problem is to cascade the exception up through the levels, with each level adding its context to the front of the message. For instance, if all the levels were in one method, they would look something like this:
try // Level 6
{
try // Level 5
{
try // Level 4
{
try // Level 3
{
DoSomethingWithList();
}
catch (Exception &VCLException)
{
throw new Exception(“Unable to “
DoSomethingWithList due to: “+
VCLException.Message);
};
}
catch (Exception &VCLException)
{
throw new Exception(“Attempt to “
“insert a service type in the “
“service type list failed due “
“to: “ + VCLException.Message);
};
}
catch (Exception &VCLException)
{
throw new Exception(“Update of “
“Package record failed due to: “+
VCLException.Message);
};
}
catch (Exception &VCLException)
{
Display(“Unable to ship package due ”
“to: “ + VCLException.Message);
};
Note that the final level actually displays the message. The resulting message looks something like:
Unable to ship package due to: Update of Package record failed due to: Attempt to insert a service type in the service type list failed due to: Unable to DoSomethingWithList due to: EListOutOfBounds (-1)
The first part of the message is what is most important to the user, but all of the information needed by support and the programmer is present as well.
The base class Exception and the specialized VCL classes derived from Exception are the real workhorses of exception management. You may not need anything more if your only intent is to pass messages up to level 6 for display and logging.
On the other hand, if you need to pass specialized information in your exception, or if your intent is to offer higher levels an opportunity to recover from specific exceptions, you may wish to create your own exception classes descending from Exception. Keep in mind the following factors when considering this:
You cannot predict the nature of higher levels or their exception handling policy. Your exceptions may not reach the level for which they are intended without being changed into a generic exception, in which case your special type and message information may be lost.
Levels above the one that calls the method where the exception originates often will not want to know about the peculiarities of your specific exception class. Make sure your special exception class can be informatively handled as an Exception type.
As with data modules and components, threads should not directly report their own error or exception conditions, because of their ignorance of the context of the error. Only the main thread is likely to know what the error means, whether or not and how it should be displayed. Therefore, while threads should catch their exceptions, they should only do so in order to report them to the main thread.
Basically, the following is what the thread should do:
try
{
Something();
}
catch (Exception &VCLException)
{
ReportToMainThread(Exception);
};
The report function should act as follows:
void __fastcall
ThreadClass::ReportToMainThread(
Exception &theException)
{
// A thread variable
ExceptionReference = theException;
Synchronize(
ProvideMainThreadWithException);
};
The ProvideMainThreadWithException() function might throw an Exception reference, which should now be safe because it is in the context of the main thread.
Exceptions offer an excellent opportunity to structure error handling in a way useful to users, support, and programmers. In addition, they make it possible to shift error handling out of mainline code and into less obtrusive areas of your program. They require some forethought and consistency, but the rewards are obvious.