.NET/C# exception handling: writing important code inside `finally` block with an empty `try` block

Introduction

Have you seen an empty try block before? In this situation, there are some important codes in the finally block. So, why do developers use the try {} finally {} at this way?

In this article, we will talk about this usage of the try {} finally {}.


Detail

The original link is .NET/C# 异常处理:写一个空的 try 块代码,而把重要代码写到 finally 中(Constrained Execution Regions)- walterlv. But this blog was written in Chinese, so I translate its content to English. Waterlv is an MVP(Microsoft Most Valuable Professional), and he is good at .NET Core\WPF\.NET. Here is his Blog.


Empty try block

You can visit this link to read the source code of Exception, and in these codes, you can find some strange codes.

1
2
3
4
5
6
7
8
9
10
internal void RestoreExceptionDispatchInfo(ExceptionDispatchInfo exceptionDispatchInfo)
{
// some codes are omitted here.
try{}
finally
{
// some codes are omitted here.
}
// some codes are omitted here.
}

It’s easy to find that there is an empty try block, and some important codes are written in the finally block. So, why do Microsoft’s developers write an empty try block?

And here is the annotation for this try block:

We do this inside a finally clause to ensure ThreadAbort cannot be injected while we have taken the lock. This is to prevent unrelated exception restorations from getting blocked due to TAE.

That means, writing the code inside the finally clause is to avoid Thread.Abort method to interrupt the code. The execution of the Thread.Abort method is managed by CLR, and finally is also managed by CLR. And according to the CLR mechanism, finally block will not be interrupted by the Thread.Abort method.

The code in .NET core and .NET Framework are the same.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// This is invoked by ExceptionDispatchInfo.Throw to restore the exception stack trace, corresponding to the original throw of the
// exception, just before the exception is "rethrown".
[SecuritySafeCritical]
internal void RestoreExceptionDispatchInfo(System.Runtime.ExceptionServices.ExceptionDispatchInfo exceptionDispatchInfo)
{
bool fCanProcessException = !(IsImmutableAgileException(this));
// Restore only for non-preallocated exceptions
if (fCanProcessException)
{
// Take a lock to ensure only one thread can restore the details
// at a time against this exception object that could have
// multiple ExceptionDispatchInfo instances associated with it.
//
// We do this inside a finally clause to ensure ThreadAbort cannot
// be injected while we have taken the lock. This is to prevent
// unrelated exception restorations from getting blocked due to TAE.
try{}
finally
{
// When restoring back the fields, we again create a copy and set reference to them
// in the exception object. This will ensure that when this exception is thrown and these
// fields are modified, then EDI's references remain intact.
//
// Since deep copying can throw on OOM, try to get the copies
// outside the lock.
object _stackTraceCopy = (exceptionDispatchInfo.BinaryStackTraceArray == null)?null:DeepCopyStackTrace(exceptionDispatchInfo.BinaryStackTraceArray);
object _dynamicMethodsCopy = (exceptionDispatchInfo.DynamicMethodArray == null)?null:DeepCopyDynamicMethods(exceptionDispatchInfo.DynamicMethodArray);

// Finally, restore the information.
//
// Since EDI can be created at various points during exception dispatch (e.g. at various frames on the stack) for the same exception instance,
// they can have different data to be restored. Thus, to ensure atomicity of restoration from each EDI, perform the restore under a lock.
lock(Exception.s_EDILock)
{
_watsonBuckets = exceptionDispatchInfo.WatsonBuckets;
_ipForWatsonBuckets = exceptionDispatchInfo.IPForWatsonBuckets;
_remoteStackTraceString = exceptionDispatchInfo.RemoteStackTrace;
SaveStackTracesFromDeepCopy(this, _stackTraceCopy, _dynamicMethodsCopy);
}
_stackTraceString = null;

// Marks the TES state to indicate we have restored foreign exception
// dispatch information.
Exception.PrepareForForeignExceptionRaise();
}
}
}

Constrained execution regions

This usage is introduced in Microsoft’s official document, for more detail, visit Reliability Best Practices.

Doing so instructs the just-in-time compiler to prepare all the code in the finally block before running the try block. This guarantees that the code in the finally block is built and will run in all cases. It is not uncommon in a CER to have an empty try block. Using a CER protects against asynchronous thread aborts and out-of-memory exceptions. See ExecuteCodeWithGuaranteedCleanup for a form of a CER that additionally handles stack overflows for exceedingly deep code.

So, we can use the try - finally to make a constrained execution region, then the code inside finally clause will be executed reliably.


References