Exceptions among Exceptions in .NET

Table of contents

Introduction

At some point, I learned that exceptions in C# (which I love a lot) and, subsequently, overall in .NET, can behave differently. It is even more curious than that: not all exceptions, and not always, can be handled and intercepted, which seems to completely contradict the essence of the try-catch-finally pattern.

While delving into this, I kept finding more and more exceptions among exceptions that actually 'beat' the try-catch-finally pattern. By the time my list had grown to seven items, I suddenly realized there was no place anywhere to find them all at once, as any existing article would take two or three cases at most.

This is why I finally settled down to writing this blog post.

In theory

Applicability limits

Before talking about exceptions and their handling with try-catch-finally, let us outline the limits within which these scenarios may be applied.

The first case, quite an obvious one, is when you want to handle an exception and provide a try-catch-finally construction manually.

try
{
	//Some work here
}
catch (Exception)
{
  //Some exception handling
}
finally
{
  //Some work that we expect always to be done
}

The second case is when you use expressions that, when compiled, expand into try-finally, i.e., they are essentially syntactic sugar. For .NET, these are using, lock and foreach:

using (var resourseWorker = new ResourseWorker())
{
  // Some work with disposable object
}
lock (x)
{
  // Your code...
} 
foreach (var element in enumerableCollection)
{
  //Your code...
}

Similar constructions with try-catch-finally:

var resourseWorker = new ResourseWorker();
try {
  //Some work with disposable object
}
finally {
  ((IDisposable)resourseWorker).Dispose();
}
bool __lockWasTaken = false;
try {
  System.Threading.Monitor.Enter(x, ref __lockWasTaken);
  //Your code...
}
finally {
    if (__lockWasTaken) System.Threading.Monitor.Exit(x);
}
var enumerator = enumearbleCollection.GetEnumerator();
try {
  while (enumerator.MoveNext()) {
    var element = enumerator.Current;
    //Your code...
  }
}
finally {
  //Dispose enumerator if needed
}

The third case is when you leverage libraries and frameworks that can use any of the above expressions in their code. Thus, this makes it almost impossible to find an application for which this would not be relevant; it means you always have protected blocks, which, assumingly, always get executed.

For starters, however, we should highlight that the trу-catch-finally expression is a very stable one. Below, there are some cases showing how it properly works.

Throwing an exception from a catch block

try
{
  throw new Exception("Exception from try!");
}
catch (Exception)
{
  throw new Exception("Exception from catch!");
}
finally
{
  Console.WriteLine("Yes! It will be executed and logged");
}

This is the case most of you are likely to have already come across. According to my survey stats, however, some people believe the finally block here may not get through. Still, as far as we can see in this sample code, finally does get executed regardless when and in which block the application's run is interrupted.

Bypass finally with goto

var counter = 0;
StartLabel:
Console.WriteLine("Start\n");
try
{
  if (counter++ == 0)
  {
    Console.WriteLine("Try: 1\n");
    goto StartLabel;
  }
  Console.WriteLine("Try: 2\n");
  goto EndLabel;
}
finally
{
  Console.WriteLine("Finally\n");
}
Console.WriteLine("End: 1");
EndLabel:
Console.WriteLine("End: 2");

Console Output

Start

Try: 1

Finally

Start

Try: 2

Finally

End: 2

Intuitively, one would assume that, by using the goto operator, you will bypass the finally block; however, as you can see, it is still running. This is because of the trigger, with which .NET gets it executed: as Microsoft's documentation says, 'The statements of a finally block are always executed when control leaves a try statement'. As long as goto causes the execution area to leave the try block, the finally one is actually run. A less obvious thing: even in case goto sends the control upwards, finally will still get executed; That is why, in our example, it was executed even twice.

Eliminating a thread with Thread.Abort

void ThreadLogic()
{
  try
  {
    Task.Delay(10000).Wait();
  }
  catch (ThreadAbortException)
  {
    Console.WriteLine("Catch: Yes! It will be logged");
  }
  finally
  {
    Console.WriteLine("Finally: Yes! It will be logged");
  }
  Console.WriteLine("End: No! It will not be logged");
}
var thread = new Thread(ThreadLogic);
thread.Start();
Task.Delay(150).Wait();
thread.Abort(); 

Here, everything is pretty obvious, as the Abort method does not eliminate the thread completely, but only leads to throwing ThreadAbortException, which, subsequently, is no issue to the try-catch-finally expression.

One should also bear in mind that this method is both unreliable and dangerous. First, no one can tell you how long it will take to interrupt the thread or even whether it will be interrupted at all. Second, you do not know in advance how important the work being interrupted is and whether it can damage other threads or the entire application. This was, however, well explained by Microsoft in its documentation, quite long ago.

Getting an Exception not inherited from the System.Exception class

Basically, this seems to contradict the Exception class description, according to which System.Exception is the base class for all exceptions. This is actually true, but only for all exceptions within .NET, while you still can get an exception from non-.NET code if you use libraries based on other languages, or otherwise use fragments of other languages.

Actually, try-catch can catch such an exception, too, as long as you use catch without specifying the class. This is called general catch, and it can intercept any possible errors.

Most interesting part: why can try-catch-finally fail?

Killing a process

It seems obvious enough that if you kill process, neither catch nor finally will get executed. On the other hand, we must immediately ask two questions:

  1. How often does this happen?
  2. Can we just disregard it, following the 'no process, no issue' principle?

The first case where this is quite common is microservice architecture. The more servers you've got, the more chances are that any of them gets down for any reason, and then, sure enough, all its processes will get killed. Nowadays, when dockers and containers are gaining more and more popularity, and the application architecture almost always includes a couple, or even a few dozen, various microservices, this is becoming quite a realistic scenario.

Then, we've got mobile apps, too. It is critical for a mobile phone to work properly in any situation as a device for making calls, so the OS always reserves the right to kill any process once it feels it is running out of resources.

Both such cases lead to killing an application process, within which a try block might have been executed; subsequently, the appropriate catch and finally blocks will not run either.

Now, let's move on to our second question. If there is no process, there is assumingly no issue, as no one cares whether any code is running within a process that has already been killed. Well, in fact, you should care. In microservice architecture, a finally block may house some microservice interaction, such as closing a transaction or connection, or taking or putting something from or to the queue. There is even a more trivial thing: you might have expected that, if the operation fails, the process will run into catch and log it.

The above behaviour is contrary to the intuitive expectations of the developers: the code within finally should always get executed, and so should the catch block code, in case the try logic fails. Still, this behaviour is pretty consistent with the documentation, as the catch block is executed once the exception occurs. It does not have to be a .NET exception, but it must be recognized by Windows' Structured Exception Handling (SEH); at the same time, when a process gets killed, no exception is thrown. As for finally, the documentation says it will be executed once we get through the try block; when a process is killed, however, there is just not enough time to get through at all.

Let us now move on to a less obvious case.

FailFast & Exit

.NET has the Environment.FailFast method, which logs an event to the Windows log upon being called, throws ExecutionEngineException and then immediately kills the process within which it has been called. This naturally leads to the try-catch-finally expression being totally inefficient here.

Curiously enough, ExecutionEngineException is an ordinary exception that does not have any special properties, which means it is handled like any other.

Another static environment class method is Environment.Exit that also kills the process within which it is called; as a result, the finally block does not get triggered. This is somewhat less obvious, since, formally, the Exit method informs the application is complete, with some code, possibly even successful. Yet, the expression still cannot handle it.

Corrupted State Exception

Corrupted State Exception, or CSE, belongs to a class that is a part of SEH; consequently, the CLR and the application you are developing should know when this exception occurs. Despite this, however, it is not intercepted or processed by try-catch-finally.

Not handling this exception is a deliberate decision taken by Microsoft once .NET 4.0 came into play because of the very nature of the CSE.

As the name suggests, this exception occurs due to a corrupted state of an application, namely, due to the corrupted application's memory, either on the heap or on the stack. This means one is totally unaware how the application is going to behave in case it continues working. This is why Microsoft arrived to the conclusion that it is safer to simply shut the app down without trying to process anything.

The most common causes of memory corruption are the following:

  1. Using unsafe code in an unwary manner.
  2. Something going wrong with Windows while processing a process.
  3. .NET engine bug.

Nevertheless, even though this exception is not handled, we can still learn it happened. Before killing a process, .NET always logs an entry in the Windows Event Viewer and dumps the application state into the logs. There, one may always find information on what happened, and, in case the issue is not that critical, even restore the application from the point at which the exception was thrown.

You can even intercept this exception, although Windows warns you against it, by using [HandleProcessCorruptedStateExcepionsAttribute], which gets attached to the method, making the try-catch-finally expression actually catch the Corrupted State Exception.

In addition, you can apply this logic to the entire application, if so required, through adding the <legacyCorruptedStateExceptionsPolicy> element to the configuration. This works, however, only for .NET Framework, as .NET Core is unable to catch a corrupted state exception and, as a result, will ignore it, even formally having the above attribute.

As we specified above, a corrupted state exception is not a specific exception, but an entire class of such. If you study .NET sources, you will find out there are eight types of them in the IsProcessCorruptedStateException method:

  1. STATUS_ACCESS_VIOLATION
  2. STATUS_STACK_OVERFLOW
  3. EXCEPTION_ILLEGAL_INSTRUCTION
  4. EXCEPTION_IN_PAGE_ERROR
  5. EXCEPTION_INVALID_DISPOSITION
  6. EXCEPTION_NONCONTINUABLE_EXCEPTION
  7. EXCEPTION_PRIV_INSTRUCTION
  8. STATUS_UNWIND_CONSOLIDATE

InvalidProgramException

This exception is thrown when the CLR is unable to read and interpret the intermediate bytecode. As MSDN says, getting this exception usually means there is a bug in the compiler that generated the code that led to the error.

There is another way to get this exception, namely with dynamic code generation through ILGenerator. Since the intermediate code is created dynamically, it may be invalid, so attempting to execute it will also lead to InvalidProgramException.

AppDomain & FirstChanceException

As you probably know, .NET has a global AppDomain object; apart from that, one can leverage a number of subscriptions to various events. Our particular case leads us to exception-related subscriptions, which are two:

  1. FirstChanceException, which is triggered when any exception is thrown into the application for the first time, and.
  2. UnhandledException that gets active when any exception is thrown but does not get caught.

Let us see now how FirstChanceException works. When an exception is thrown, .NET runs all subscriptions against that event, and only then the application determines how to handle the exception, i.e. which finally blocks should get executed, from which moment the app should continue its execution, and whether the exception is to be intercepted. Subsequently, if an exception occurs in any of the subscription methods and it is not caught, .NET will never get back to handle the original exception, and the application will not transfer control to the catch and finally blocks:

AppDomain.CurrentDomain.FirstChanceException += (sender, eventArgs) =>
{
  Console.WriteLine($"Log from FirstChanceException: {eventArgs.Exception.Message}\n");
  if (eventArgs.Exception.Message != "Exception from FirstChanceException!")
    throw new Exception("Exception from FirstChanceException!");
};

In this code sample, throwing an exception is under a condition; otherwise, this would lead to eternal recursion, since FirstChanceException responds to exceptions within itself, too.

AppDomain.CurrentDomain.FirstChanceException += (sender, eventArgs) =>
{
  Console.WriteLine("Log from FirstChanceException: " + eventArgs.Exception.Message);
  throw new Exception("Exception from FirstChanceException");
};

Consequently, the very fact of using FirstChanceException is extremely dangerous, since any internal exception will lead to the entire process being instantly killed. There is a way, however, to partially bypass it by wrapping up all method logic into a try block:

AppDomain.CurrentDomain.FirstChanceException += (sender, eventArgs) =>
{
  try
  {
    Console.WriteLine($"Log from FirstChanceException: {eventArgs.Exception.Message}\n");
    if (eventArgs.Exception.Message != "Exception from FirstChanceException!")
      throw new Exception("Exception from FirstChanceException!");
  }
  catch { /* ignored */ }
}; 

You still have to remember this does not mean there will be no eternal recursion at all:

AppDomain.CurrentDomain.FirstChanceException += (sender, eventArgs) =>
{
	try
  {
    Console.WriteLine("Log from FirstChanceException: " + eventArgs.Exception.Message);
    throw new Exception("Exception from FirstChanceException");
  }
  catch { /* ignored */ }
}; 

Bottomline: never use this subscription to implement business logic. However, if it is that dangerous, why might one ever need it? Well, for instance, it may come in handy for logging and collecting statistics on exceptions. Nevertheless, there are situations where the logs are unavailable, and any logging event will also be triggering exceptions. For this reason, you might want to use it only outside your production environment.

UnhandledException, the second exception-related subscription, is, on the contrary, completely safe and will not disrupt your application's execution. With this subscription, the events are called after .NET has been completely done with the exception and made sure nothing handles it and all finally blocks will get executed.

Finally, an interesting fact: both subscriptions are not triggered with Corrupted State Exceptions (CSEs).

OutOfMemoryException

OutOfMemoryException is no different from all other exceptions in terms of behaviour.

Let's take a look at a particular case, however: suppose we had a finally or catch block running and then failing for some reason. Generally, for a developer, this behaviour is no better than when such blocks do not run at all, which is possible in case you get an exception once a block starts to get executed. On the other hand, the logic behind a finally block is usually quite minimalistic and reliable, as its core task is to always be true. Then, we've got another question: can we get an exception within a block through no fault of its code?

To answer this question, you will need to understand how OutOfMemoryException works. As a rule, we arrive to a try block and allocate a huge amount of memory, which leads to an exception being thrown and successfully handled.

What happens, however, if we allocate a near-critical amount of memory before we actually get to the try block, and then allocate some more memory enough to cause an exception?

double[] array1 = new double[200_000_000];
double[][] array2 = new double[1000][];
double[] array3;
int i = 0;
try
{
  Console.WriteLine("Try: Yes! It will be logged");
  for (; i < array2.Length; i++)
    array2[i] = new double[100_000];
  Console.WriteLine("Try: No! It will be not logged");
}
catch (Exception e)
{
  Console.WriteLine($"Catch, i = {i}: Yes! It will be logged, but value of “i” always will be different in range from 320 to 380");
}
finally
{
  array3 = new double[500_000];
  Console.WriteLine("Finally: No! It will be not logged");
} 

This code will cause the finally block to run, but not execute, as it just won't have enough memory for that. As a result, the finally block will throw an exception, despite the code within the block not containing anything problematic and looking quite safe and reliable.

As the console output says, the value of the counter variable will always be different when OutOfMemoryException is thrown for the first time. This is due to the fact that this exception is sort of unpredictable: one cannot know in advance when there will not enough memory, since it depends on many factors, such as the OS, the internal .NET engine, and the garbage collector.

Let's also point out how much memory you can use to avoid getting OutOfMemoryException.

Maximum object size:

  • .NET Framework: 2GB. If you try to create a larger object, you will get an exception; however, you can reconfigure this in the <gcAllowVeryLargeObjects> file.
  • .NET Core: no limitations in terms of the object size.

Maximum amount of virtual memory being allocated:

  • 32-bit process and 32-bit system — 2GB,
  • 32-bit process and 64-bit system — 4GB,
  • 64-bit process and 64-bit system — 8TB.

In the example above, we had to compile the application as a 32 bit one in order to stabilize getting the exception; otherwise, the maximum amount of memory being allocated would reach 8TB.

StackOverflowException

The first thing coming to mind here is that we can repeat the idea we used for OutOfMemoryException: fill the stack almost completely, get into the try block, get an exception, end up in the finally block with the stack almost completely full, and get an exception again.

In fact, this will not work, as StackOverflowException cannot be intercepted with a try-catch-finally expression starting .NET Framework 2.0.

If you were reading attentively enough, you would have already noticed that, formally, StackOverflowException is of the same nature as Corrupted State Exception. If this is actually so, then why should we specifically mention it?

We should, however, as it behaves in a completely different manner compared to other CSEs. The first difference is that its handling became no longer supported starting from version 2, not version 4. Second, you cannot change this behaviour with any attributes and, as a result, StackOverflowException is never intercepted.

Well, formally speaking, not like 'never ever', as there are hacks, such as hosting .NET in an unmanaged application or overriding the behaviour in case of stack overflow. This article, however, targets specific situations that can suddenly happen in any application, so there is no point in considering such exotic options.

So, it is well known that the most common root cause of stack overflow is redundant method nesting or recursion. Therefore, the most obvious way to avoid this exception is to reduce method nesting or use tricks that allow you to expand recursion into loops.

However, one should remember there is another reason for getting this exception. As we all know, the performance of any garbage-collector language has a core weak spot: the garbage collector itself. Consequently, if you allocate memory on the stack, and not on the heap, when applicable, you will make it easier for the collector to complete its task and improve performance. For instance, you can allocate arrays on the stack using Span and stackalloc. On the other hand, abusing this technique or allocating too large arrays will also lead to a stack overflow exception, which, as you might remember, is uncompromising and cannot be intercepted in any way.

Span<int> stackSpan1 = stackalloc int[150000];
Span<int> stackSpan2 = stackalloc int[150000];
Span<int> stackSpan3 = stackalloc int[150000]; //Here I got OutOfMemoryException 
Span<int> stackSpan4 = stackalloc int[150000]; 

This does not mean, however, that we should not be using this technique at all. We actually can do so, for instance, with RuntimeHelper, namely the following two of its methods:

  1. EnsureSufficientExecutionStack that checks whether there is enough space on the stack to execute an average .NET function. If not, InsufficientExecutionStackException is thrown.
  2. TryEnsureSufficientExecutionStack, similar to the previous method, which returns a boolean value: whether there is there any space on the stack.

You might have just thought that enough stack space for an average function sounds quite inappropriate for a technical article, and, if you actually have, you are correct. This is, however, exactly what MSDN's documentation says, without specifying any exact amount of memory. There is also a post on .NET special exceptions with some figures and explanations, although without any references. Please note that I did not check this information and did not find any proofs in the official documentation. Anyways, the figures go as follows.

  • .NET Framework: 50% of the stack size
    • x86: 512 KB
    • x64: 2 MB
  • .NET Core: 64/128 KB

As we can see, .NET Core is less prone to panic and will notify us much later that the stack is running out, which allows you to manage the stack memory in a much more complete and efficient manner.

bool isStackOk = RuntimeHelpers.TryEnsureSufficientExecutionStack();
Span<int> stackSpan = isStackOk ? stackalloc int[10000] : new int[10000]; 

For instance, with this in mind, you can allocate stack memory only in case you can really afford it.

Conclusion

Most people get disappointed of try-catch-finally expressions because they misinterpret their trigger rules.

A developer would expect that, if the app fails, the catch block will be executed in most cases, while the finally is just bound to always get executed. In fact, as we can see, catch is only executed in case an exception is thrown, while finally gets triggered once the .NET execution area leaves the try-catch blocks, and only in case the CLR is in good health.

However, the question whether one should avoid exceptions within exceptions is still here to stay. If you asked me, I would say there is not much point in thinking about it very much in advance, since all situations we described above are quite rare and exotic. It is much wiser to just keep in mind that the rules of the game may change, and even the most stable and reliable expression in a language may fail. I have been working as a developer in the business domain for six years, and encountered such an issue only once. However, as I knew what exactly happened and why, I managed to save many hours I would have otherwise spent on debugging. Now, I do hope my research will also be useful for you.

There is also another answer to the question above, which has something to do with microservices and the architecture design principle saying that the fault tolerance of the system is superior to the fault tolerance of any single component. However, this is a completely different story.

You Might Also Like

Blog Posts Distribution of Educational Content within LMS and Beyond
October 16, 2023
When creating digital education content, it is a good practice to make it compatible with major LMSs by using one of the widely used e-learning standards. The post helps to choose a suitable solution with minimal compromise.
Blog Posts Story of Deprecation and Positive Thinking in URLs Encoding
May 13, 2022
There is the saying, ‘If it works, don’t touch it!’ I like it, but sometimes changes could be requested by someone from the outside, and if it is Apple, we have to listen.
Blog Posts The Laws of Proximity and Common Region in UX Design
April 18, 2022
The Laws of Proximity and Common Region explain how people decide if an element is a part of a group and are especially helpful for interface designers.