Root > Advanced topics > Nested/Chained exceptions

Nested/Chained exceptions

Previous pageReturn to chapter overviewNext page   

Building good error handling facility in your applications

Application 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
  // Declare root class for all exceptions from some framework
  EFrameworkError = class(Exception);
    // Declare subclass for all file-related errors
    EFrameworkFileError = class(EFrameworkError)
    private
      // This subclass will hold additional information: file name
      FFileName: String;
    public
      constructor Create(const AMessage, AFileName: String);
    end;
      // Declare more subclasses to specify errors even more
      EFrameworkFileCreateError = class(EFrameworkFileError);
      EFrameworkFileOpenError = class(EFrameworkFileError);
      EFrameworkFileReadError = class(EFrameworkFileError);
      EFrameworkFileWriteError = class(EFrameworkFileError);
    // ... other errors from framework
    EOtherFrameworkError = class(EFrameworkError);
    // ...
 
{ EFrameworkFileOpenError }
 
constructor EFrameworkFileError.Create(const AMessage, AFileName: String);
begin
  inherited Create(AMessage);
  FFileName := AFileName;
end;

 

// ------------------------------------------------------------
 
type
  // A sample class from framework
  TFrameworkDocument = class
  private
    FFileName: String;
    FFile: TFileStream;

 
  protected
    procedure Open;
    procedure ReadFileInfo;
    // ...
    procedure Close;

 
  public
    constructor Create(const AFileName: String);
    destructor Destroy; override;

 
    property FileName: String read FFileName;
  end;
 
resourcestring
  // Error messages for framework errors
  rsUnableToOpenFile    = 'Sorry, unable to open file: %s';
  rsErrorReadingHeader  = 'Sorry, unable to read file header from: %s';
 
{ TFrameworkFile }
 
constructor TFrameworkDocument.Create(const AFileName: String);
begin
  inherited Create;
  FFileName := AFileName;
  Open;
end;
 
destructor TFrameworkDocument.Destroy;
begin
  Close;
  inherited;
end;
 
procedure TFrameworkDocument.Open;
begin
  Close;
  try
    FFile := TFileStream.Create

      (FileName, fmOpenReadWrite or fmShareDenyWrite);
  except
    // Catch any low-level errors (such as "access denied", etc.)
    // and wrap into framework errors with supplying more information
    // (such as file name in this example)
    Exception.RaiseOuterException

      (EFrameworkFileOpenError.Create

        (Format(rsUnableToOpenFile, [FileName]), FileName));
  end;
end;
 
procedure TFrameworkDocument.Close;
begin
  FreeAndNil(FFile);
end;
 
procedure TFrameworkDocument.ReadFileInfo;
// ...
begin
  try
    FFile.Position := 0;
    FFile.ReadBuffer({ ... });
  except
    // Catch any low-level errors (such as "error reading file", etc.)
    // and wrap into framework errors with supplying more information
    // (such as file name in this example)
    Exception.RaiseOuterException(

      EFrameworkFileReadError.Create(

        Format(rsErrorReadingHeader, [FileName]), FileName));
  end;
end;

 

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:

Errors from TFileStream object are low-level errors. They are nested into framework exception classes.
Errors from framework are triggered by low-level errors.

 

 

What are nested/chained exceptions

Chained 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 feature

Default 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 feature

EurekaLog 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
try
  raise Exception.Create('Inner Exception');
except
  on E: Exception do
    Exception.RaiseOuterException(Exception.Create('Outer Exception'));
end;
 
// "Unofficial" (implicit) chaining
try
  raise Exception.Create('Inner Exception');
except
  on E: Exception do
    raise Exception.Create('Outer Exception');
end;

 

The difference:

Explicit chaining will keep the inner exception object alive until the very end of handling/processing;
Implicit chaining will delete the inner exception object immediately after throwing an outer exception.

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;
begin

  // Check if the exception info has an associated exception object:
  if (AExceptionInfo.ExceptionObject <> niland
     AExceptionInfo.ExceptionNative and

     (TObject(AExceptionInfo.ExceptionObject).InheritsFrom(Exception)) then

    E := Exception(AExceptionInfo.ExceptionObject)

  else

    E := nil

 

  // Do something with E here...

 
end;
 
initialization
  RegisterEventExceptionNotify(nil, AlterException);
end.

 

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:




Send feedback... Build date: 2023-09-11
Last edited: 2023-06-28
PRIVACY STATEMENT
The documentation team uses the feedback submitted to improve the EurekaLog documentation. We do not use your e-mail address for any other purpose. We will remove your e-mail address from our system after the issue you are reporting has been resolved. While we are working to resolve this issue, we may send you an e-mail message to request more information about your feedback. After the issues have been addressed, we may send you an email message to let you know that your feedback has been addressed.


Permanent link to this article: https://www.eurekalog.com/help/eurekalog/chained_exceptions.php