Building good error handling facility in your applicationsApplication with good and thoughtful error handling will probably use layered error handling scheme.
Layered error handling architecture
That is, errors will first be raised as so-called low-level errors. Low-level error indicates exact reason for failure - such as failures inside OS functions, hardware exceptions (access violation, etc.) and so on. Usually, you're interested in low-level errors to get precision information about failure (so you can resolve it).
Unfortunately, low-level errors suffers from the following: 1. Low-level errors from one piece of code are not different from errors from another piece of code. I.e. they are almost identical for any code in your application (access violation from any function is represented by the same EAccessViolation exception class). This does not allow you to build error handling logic which can differentiate between errors. 2. Error message of low-level errors are, well, "low-level" (such as "Range Check Error", "Index out of bounds", "Access Violation at address ... in module ... read ...", etc.). Such error messages may be good for diagnostic purposes for developers, but they are not user friendly. Normal users of your application could not read them. It would be better to show more friendly messages (such as "Sorry, I can not open your file XYZ, it seems damaged").
Application can use medium- and hi-level errors to address these issues. Usually this means that framework should handle low-level errors and translate them into framework-specific errors. Framework-specific errors can include additional information about errors. In other words, low-level errors are "surfaced" to framework code and are transformed into framework exception classes. For example:
type
// ------------------------------------------------------------ (FileName, fmOpenReadWrite or fmShareDenyWrite); (EFrameworkFileOpenError.Create (Format(rsUnableToOpenFile, [FileName]), FileName)); EFrameworkFileReadError.Create( Format(rsErrorReadingHeader, [FileName]), FileName));
Note: Exception.RaiseOuterException construct is available only in RAD Studio 2009+. Older versions of Delphi and C++ Builder have to use "raise" and "throw" keywords.
This example illustrates a good exception handling approach for frameworks and any middle-level code. Re-raising low-level errors as framework exceptions allows you to specify more information about error: such as file name and operation kind (i.e. open, read, etc.). Such information may not be available for low-level errors. This approach also allows you to provide more descriptive error message.
In this example:
What are nested/chained exceptionsChained exception is an exception which occurs during handling of another exception. That "another" (original) exception is called "nested exception". For example:
try // Low-level error (a.k.a. original, first, bottom, inner, nested, root) raise ERangeError.Create('Invalid item index'); except // High-level error (a.k.a. introduced, last, top, outer, chained) raise EFileLoadError.CreateFmt('Error loading file %s', [FileName]); end;
As you can see, low-level exception (nested) is the exception you're interested in. It indicates a reason for failure. This is what you typically want to be logged. Chained exception is triggered by original exception and provides more descriptive error message. So, you typically want to show it to user as error message.
Thus, typically you want first exception to be logged, but last exception to be shown to end user. Classic/default Delphi and C++ Builder behavior is to work only with last exception always.
Delphi 2009+ only: starting with Delphi 2009 - there was new features introduced to exceptions in RTL. Support for chained exceptions was added. There are new properties BaseException and InnerException as well as special raising construct. In this model, you need to use RaiseOuterException or ThrowOuterException to preserve original exception when raising new exception. EurekaLog implements similar model with the same properties, except it doesn't require you to use special raising construct. Any exception raising automatically saves previous (original) exception in InnerException property. This feature available on all supported IDE versions. See also the important considerations section below.
EurekaLog nested/chained exception tracking featureDefault behavior of Delphi/C++ Builder: show last (i.e. chained) exception and hide original (i.e. nested) exception. This behavior is what you want for user, but it's not what you want for diagnostic purposes. EurekaLog has the feature to change/customize this behavior. Options on "Nested exceptions" page allow you to customize EurekaLog behavior related to nested/chained exceptions.
Default settings for EurekaLog is to log original (nested) exception, but show chained exception to user. For example, if you run example code from above for non-existed file:
Error message from chained exception is shown to user It's descriptive and user-friendly
Original (nested root) exception is stored into bug report Its error message indicate low-level failure reason
Call stack also indicate original exception
Important considerations for using nested/chained exceptions tracking featureEurekaLog requires ability to track life-time of exception objects for this feature. Default EurekaLog options are configured to allow it. However, if you manually change the EurekaLog options - you may disable features that allows EurekaLog to track life-time of exception objects. Such options includes:
For example, if you're using Delphi 7 and disable both "Enable extended memory manager" and "Use low-level hooks" options - then EurekaLog will be unable to detect when exception object is destroyed. Thus, tracing nested/chained exceptions feature will not function properly. This means that you may get information about wrong exception in your bug reports.
It's recommended to test your application when you alter "Enable extended memory manager", "Use low-level hooks" or "Capture stack only for exceptions from current module" options. If your application configuration fails to store proper information - please, switch nested/chained exceptions options into "Classic" positions instead of (default) "Recommended" positions.
Additionally, if you enable support for nested/chained exceptions - be aware about life time of inner exceptions. For example:
// "Official" (explicit) chaining
The difference:
This difference is important if you are trying to access the original exception object from your own code. For example:
// Some custom event handler: procedure AlterException(const ACustom: Pointer; AExceptionInfo: TEurekaExceptionInfo; var AHandle: Boolean; var ACallNextHandler: Boolean); var E: Exception; // Check if the exception info has an associated exception object: (TObject(AExceptionInfo.ExceptionObject).InheritsFrom(Exception)) then E := Exception(AExceptionInfo.ExceptionObject) else E := nil;
// Do something with E here...
E will be assigned for explicit chaining and E will be nil (unassigned) for implicit chaining in the example above.
For this reason we recommend to use properties of TEurekaExceptionInfo as much as possible, avoiding accessing the exception object.
See also:
|