The Back Side of Exceptions

.NET EXCEPTIONS: The Back Side of Exceptions

This article assumes you are familiar with exception handling in .NET and with C# (or a similar language).

Introduction

Error handling is a general topic. This article is concerned with the strategy based on exceptions (which are a universal mechanism for raising and reporting an error) in relation to .NET. Here, you will learn about two problems of exception handling (that are not covered enough in existing documentation) and see how to prevent some insidious situations, such as misidentification of an exception leaving the program in an inconsistent state, or a loss of exception information. The article has some theoretical flavor but may also be helpful in practice; the effects described here are quite probable for large programs, especially for multi-module applications developed by different people.

Introducing Exception Handling Problems

Exceptions are used everywhere in .NET. Any language instruction or function call may throw an exception. In comparison with the result code-based strategy, this method gives no chance to a situation where you have missed an error check and the execution continues normally in spite of that error; an exception will be thrown instead. An exception-based API (what .NET is) is more advanced than the result code-based one (at least for user mode coding). Of course, you should be ready to catch an exception in any suspected place (where it may really occur).

.NET offers a rich hierarchy of exception classes. In conjunction with the compiler, it uses a very efficient exception handling mechanism, so execution performance is not affected when a possible exception doesn’t occur (only the actual handling is expensive). .NET-oriented languages provide different constructions relative to exception handling. And so forth. Nevertheless, some shortcomings also exist. Look at this abstract but typical code example, like many programmers sometimes write (this snippet and all the others are in C#):

try
{
   //< Do some complex action(s) >
}
catch (Exception ex)
{
   //< Inform a user about the error >
}
//< Continue the execution, implying that everything is OK >

In the strategy based on result codes, you possibly have to write a lot of checks instead, to process all bad situations correctly. Maybe you will encounter a very serious error, so it will require some special action (not only to unwind the stack and to inform the user). Maybe you’ll decide to stop further execution. It is very naïve to think that such a simple method of exception catching that you have seen just now always resolves all the difficulties you could have in the hard result code-based method of error checking. It is nearly like you decide to terminate an auxiliary thread in an arbitrary place by a TerminateThread call (Win32 function): Some data in your program may become inconsistent (meaning that further execution is unsafe). Exception catching may cause a similar situation. Such a case is a relatively rare incident, but it can take place and someday will occur (especially if you use more or less general kinds of catch blocks, like catching Exception or SystemException; it’s not easy to avoid them everywhere).

As for the example above, of course you can reorganize it. Apply a compound catch block: catch exception classes (objects) from the most to the least specific error. Even several hierarchy branches can be used. (Also, the most general catch block can be provided at the end.) You can enforce the try block with inner try-catch-finally sections. But, there is no special language construction and there is no clear recommendation (a universal rule) that allows you to certainly avoid program data inconsistency in coding with exception handling. MSDN documentation (at least the one shipped with Visual Studio® 2005) only gives a little notice about side effects in connection with exceptions.

You are approaching the problem. This article explains an insidious side effect of exception catching. It will show the danger and introduce some terms (abstractions) that will allow you to understand what problem situations may exist. These abstractions are helpful to detect rare, unexpected errors that can cause an unrecoverable program sate, and handle them as fatal (so you will always be sure that your application is well). A separate section of the article discusses another back side of exceptions—a possible loss of exception information about the original error.

The First Problem: Implicit Interruption of Code Flow as a Result of the Misidentification of Some Unexpected Exception

1. Situation where the caught abnormal exception is confused with another one, considered as a normal case

An insidious side effect may occur under some circumstances. Suppose there are three code levels: MIDDLE LEVEL (you are currently coding in), LOWER LEVEL (you rely on), and UPPER LEVEL (that calls you). Consider a logical piece of code you have to implement as a transaction. It doesn’t mean you must be able to roll back all the actions; you only need to guarantee that your code path cannot be implicitly interrupted and to stay incomplete as the result. The term “transaction” is used specifically to describe some break-critical code flow; for example, an obligatory setting of a job completion flag at the end of the started job. Another classic example of such uninterruptible complex action is a money transfer operation from one monetary account to another. Such an action is performed as two inseparable subsequent steps named Withdraw and Add; both must succeed (otherwise, the states of the accounts are corrupted).

Thus, to perform a so-called transaction, you guard all the places where exceptions are expected with try-catch-finally blocks. You normally do not guard those regions where no exception is expected. You simply imply: If such abnormal exception will be thrown (implicitly by your code, or somewhere from the deeper level), let an upper level handle it (or let it be a fatal error). You don’t want to handle it at this level; let the default exception handler, for example, catch it.

This strategy usually works well, but two simultaneous conditions will reveal its defect:

  1. The lower level code abnormally throws an exception that is absolutely unexpected, or your code implicitly causes such an exceptional situation. (For the lower level code, suppose it was not documented because the primary error cause is hidden somewhere in the depths.)
  2. The upper level code (that called you) catches an exception of a class to which the exception of your lower level code belongs, and handles it as a normal case.

What do you have? The abnormal exception from the deeper level is concealed at the upper one, and the execution continues. Some data in the so-called transaction became inconsistent (because you’ve maybe interrupted it in the middle). The transaction could have access to an instance, static, or in-stack located data, which are now corrupted. But, the upper level code thinks that everything is okay (and thinks it has handled some normal error). Thus, the side effect exists. See Listings 1 and 2.

Listing 1: Transactional code at the middle level

void MiddleLevelMethod()
{
   : : : : : : : : : :
   // === Begin of "transaction" ===
   : : : : : : : : : :
   try
   {
      : : : : : : : : : :
      LowerLevelUnreliableMethod();
      // can cause ExcpectedException_1 or ExcpectedException_2
      : : : : : : : : : :
   }
   // handling ExcpectedException_1
   catch (ExcpectedException_1 ex) { : : : : : }
   // handling ExcpectedException_2
   catch (ExcpectedException_2 ex) { : : : : : }
   : : : : : : : : : :
   LowerLevelReliableMethod(); // <-- UnexpectedException (derived
                               //     from SomeException)
   : : : : : : : : : :
   // === End of "transaction" ===
   : : : : : : : : : :
}

Listing 2: Unexpected exception causing confusion at the upper level

bool UpperLevelMethod()
{
   : : : : : : : : : :
   try
   {
      : : : : : : : : : :
      SomeUnreliableMethod();    // can cause SomeException
      : : : : : : : : : :
      MiddleLevelMethod();       // can't cause exceptions
      : : : : : : : : : :
      SomeUnreliableMethod_1();
      // can cause SomeException_1 (derived from SomeException)
      : : : : : : : : : :
      SomeUnreliableMethod_2();
      // can cause SomeException_2 (derived from SomeException)
      : : : : : : : : : :
   }
   catch (SomeException ex)      // handling SomeException,
                                 // SomeException_1,SomeException_2
   {
      // Here, we can erroneously catch UnexpectedException,
      // which has broken the transaction in the MiddleLevelMethod
      // (but we don't think about such "insignificant details"
      // at this level).
      : : : : : : : : : :
      return false;
      // execution continues (but the program may be in an
      //                      inconsistent state)
   }
   : : : : : : : : : :
   return true;
}

In a world where exceptions don’t exist, almost nothing (except thread termination) can break your code path. In the world of exceptions, you should keep in mind that your instruction sequence can be interrupted at any place. Therefore, there is the need in some advanced strategy or even in special language constructions that can protect you from situations such as were described above. (All existing recommendations do not help. Rich exception hierarchy is also ineffective in connection with this problem.) The next section introduces some innovations.

2. Presenting abstract terms: FATAL EXCEPTION and MONOLITHIC CODE BLOCK

Let me introduce two helpful terms that I want that your hypothetical programming system to support to protect you from the side effect of exception catching. These abstractions will help you laterm in your own interpretations based on the existing instruments (you’ll see it in the other sections).

Fatal Exception: An exception that is processed in a special way, so when thrown somewhere, it cannot be caught anywhere but the TOP LEVEL. One exception can be fatal by its definition, but another one (which is not declared as fatal) can also be thrown in such a fatal manner.

If your program decides that it is in a catastrophic situation or something unrecoverable has occurred, it throws a correspondent meaningful exception as fatal, being insured that the application will not function further in such an inconsistent state (that could cause a very harmful result).

The so-called top level means one of the following: For the default application domain, this is a handler routine for an unhandled exception (if installed)/the default unhandled exception handler; for the non-default application domain, this is an outer catch block in the default application domain, or the default domain’s unhandled exception handler routine (if installed)/the default unhandled exception handler. For an application that runs in the default application domain (full right GUI or console app, managed service), the System.AppDomain.UnhandledException event serves as the unhandled exception handler. (In the Windows Forms library, the System.Windows.Forms.Application.ThreadException event is used to deal with unhandled exceptions originated from a UI thread, before the top level, but window message pumping can be resumed.)

In the existing infrastructure, one fatal exception exists: the System.StackOverflowException exception. It sometimes occurrs (not thrown explicitly by the code); it cannot be caught by a try-caught block. Consequently, the exception causes the process of terminating immediately. (But, our fatal exceptions are a bit more sophisticated.)

Monolithic Code Block: An uninterruptible region within a function, where the code path can not be broken implicitly, only explicit leaving of this region is admissible: via instructions like throw, break, return, and goto. If an unguarded exceptional situation occurs during monolithic code passing (ether implicitly by this monolithic code, with no use of throw keyword, or by lower level code that this monolithic code did not guard enclosing with a try-catch-finally block),this unguarded exception causes a MonolithicCodeViolationException exception. This exception is determined as fatal (it cannot be caught anywhere but at the top level). The original exception is saved in the InnerException property of the MonolithicCodeViolationException instance as a primary error cause.

(In connection with the monolithic code block, additional rules are required for threading. If some thread is calling System.Threading.Thread.Abort or System.Threading.Thread.Interrupt on a subject thread object while the subject thread is passing through its monolithic code block, the following behavior should take place: If this monolithic code has guarded itself against ThreadAbortException or ThreadInterruptedException exception with a correspondent try-catch-finally block, this exception is processed in a usual way; but, if there is no such guard, the exception is deferred until the monolithic code path finishes and the flow reaches the end of the block.)

3. Inventing special keywords that your hypothetical compiler should provide to support monolithic code blocks and fatal exceptions

Adopting the above-mentioned abstractions, imagine that your hypothetical compiler extends the C# language and it understands the following constructions (these are not true compiler syntax diagrams, but they are understandable):

    • Fatal exception declaration (whose instance can only be thrown as fatal):
[...] fatalexceptionclass DerivedExceptionClass:
      ParentExceptionClass
    • Fatal exception throwing instruction (to throw an exception as fatal):
fatalthrow ExceptionObject;
    • Monolithic code block and monolithic function (cannot be interrupted implicitly):
monolithic { [...] }
[...] monolithic Type Function( [...] ) { [...] }

Having these enhancements, rewrite the recent transactional example in a fully protected way. See Listing 3.

Listing 3: Hypothetical monolithic code block (protected from abnormal errors)

void MiddleLevelMethod()
{
   : : : : : : : : : :
   // "Transaction":
   monolithic
   {
      : : : : : : : : : :
      try
      {
         : : : : : : : : : :
         LowerLevelUnreliableMethod();
         // can cause ExcpectedException_1 or ExcpectedException_2
         : : : : : : : : : :
      }
      // Handling ExcpectedException_1 or ExcpectedException_2:
      catch (ExcpectedException_1 ex) { : : : : : }
      catch (ExcpectedException_2 ex) { : : : : : }
      : : : : : : : : : :
      LowerLevelReliableMethod();    // <-- UnexpectedException
      // (derived from SomeException)
      //  If UnexpectedException will occur -- it will be caught
      // at the top level and nowhere else, so UpperLevelMethod
      // can't accidentally catch it.
      : : : : : : : : : :
   }
   : : : : : : : : : :
}

More by Author

Must Read