1000 words
5 minutes
C# 6.0 Exception Filter and when Keyword

C# 6.0 introduces a new feature exception filter and a new keyword when. Many C# features/keywords are syntactic sugars, but exception filter/when keyword is not.

To examine this feature, a few helper methods can be created:

internal static partial class ExceptionFilter
{
private static void A() => B();
private static void B() => C();
private static void C() => D();
private static void D()
{
int localVariable1 = 1;
int localVariable2 = 2;
int localVariable3 = 3;
int localVariable4 = 4;
int localVariable5 = 5;
throw new OperationCanceledException(nameof(ExceptionFilter));
}
private static bool Log(this object message, bool result = false)
{
Trace.WriteLine(message);
return result;
}
}

These methods can make up a call stack, with some local variables. The Log method can log a Exception object and return a specified bool value.

Syntax#

The when keyword works like if. A when condition is a predicate expression, which can be appended to a catch block. If the predicate expression is evaluated to be true, the associated catch block is executed; otherwise, the catch block is ignored.

private static void Filter()
{
try
{
A();
}
catch (OperationCanceledException exception) when (string.Equals(nameof(ExceptionFilter), exception.Message, StringComparison.Ordinal))
{
}
}

In the earlier preview of C# 6.0, the if keyword was used. In the final release, if is replaced by when, because some improper format can make catch-if confusing, e.g.:

private static void Filter()
{
try
{
A();
}
catch (OperationCanceledException exception)
// {
if (string.Equals(nameof(ExceptionFilter), exception.Message, StringComparison.Ordinal))
{
}
// }
}

The above code format looks just like a if statement inside the catch block.

Now it is already March 2016, the MSDN document for C# exception filter still uses the if keyword in the examples:

image

Compilation#

Before C# 6.0, it is very common to catch an exception, then log or filter it, and re-throw:

private static void Catch()
{
try
{
A();
}
catch (Exception exception)
{
exception.Log();
throw;
}
}

C# 6.0 provides a way to log or filter an exception before catching it:

private static void When()
{
try
{
A();
}
catch (Exception exception) when (exception.Log())
{
}
}

Here the Log method will log the exception, and return false. So the catch block will not be executed.

ILSpy and ildasm (located in C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\) can be used to view the compiled IL. In the Catch method, the catch-log-throw pattern will be compiled to:

.method private hidebysig static void Catch() cil managed
{
.maxstack 2
.locals init ([0] class [mscorlib]System.Exception exception)
IL_0000: nop
.try
{
IL_0001: nop
IL_0002: call void Dixin.Console.Program::A()
IL_0007: nop
IL_0008: nop
IL_0009: leave.s IL_0017
} // end .try
catch [mscorlib]System.Exception
{
IL_000b: stloc.0
IL_000c: nop
IL_000d: ldloc.0
IL_000e: ldc.i4.0
IL_000f: call bool Dixin.Console.Program::Log(object,
bool)
IL_0014: pop
IL_0015: rethrow
} // end handler
IL_0017: ret
} // end of method Program::Catch

There is nothing new or surprising. And When method is compiled to:

.method private hidebysig static void When() cil managed
{
.maxstack 2
.locals init ([0] class [mscorlib]System.Exception exception,
[1] bool V_1)
IL_0000: nop
.try
{
IL_0001: nop
IL_0002: call void Dixin.Console.Program::A()
IL_0007: nop
IL_0008: nop
IL_0009: leave.s IL_002a
} // end .try
filter
{
IL_000b: isinst [mscorlib]System.Exception
IL_0010: dup
IL_0011: brtrue.s IL_0017
IL_0013: pop
IL_0014: ldc.i4.0
IL_0015: br.s IL_0024
IL_0017: stloc.0
IL_0018: ldloc.0
IL_0019: ldc.i4.0
IL_001a: call bool Dixin.Console.Program::Log(object,
bool)
IL_001f: stloc.1
IL_0020: ldloc.1
IL_0021: ldc.i4.0
IL_0022: cgt.un
IL_0024: endfilter
} // end filter
{ // handler
IL_0026: pop
IL_0027: nop
IL_0028: rethrow
} // end handler
IL_002a: ret
} // end of method Program::When

The catch keyword is gone, and C# when condition is compiled to a IL filter block. In the filter block, it checks if the exception is of Exception type. If so, it calls the Log method. Apparently, exception filter is not syntactic sugar. It is a CLR feature.

Runtime: stack unwinding#

The catch block and when predicate refers to the same exception object. In the following example:

internal static void Log()
{
try
{
A();
}
catch (Exception exception) when (exception.Log(true))
{
exception.Log();
throw;
}
}

In the when predicate, the Log method returns true, so in the catch block, Log will be called again. These 2 Log calls print out exactly the same information:

System.OperationCanceledException: ExceptionFilter at Dixin.Common.ExceptionFilter.D() in D:\OneDrive\Works\Drafts\CodeSnippets\Dixin\Common\ExceptionFilter.cs 21 at Dixin.Common.ExceptionFilter.C() in D:\OneDrive\Works\Drafts\CodeSnippets\Dixin\Common\ExceptionFilter.cs 12 at Dixin.Common.ExceptionFilter.B() in D:\OneDrive\Works\Drafts\CodeSnippets\Dixin\Common\ExceptionFilter.cs 10 at Dixin.Common.ExceptionFilter.A() in D:\OneDrive\Works\Drafts\CodeSnippets\Dixin\Common\ExceptionFilter.cs 8 at Dixin.Common.ExceptionFilter.Log() in D:\OneDrive\Works\Drafts\CodeSnippets\Dixin\Common\ExceptionFilter.cs 91

Apparently, in both cases, the exception object’s StackTrace property has the call stack of A/B/C/D methods, as expected.

The real difference is the CLR stack (not the exception object’s StackTrace string property). To demonstrate this, set 2 breakpoints at 2 Log calls:

image

When the exception filter is executed:

image

The current stack (again, not the exception object’s StackTrace property) is:

ConsoleApplication2.exe!Dixin.Common.ExceptionFilter.Log() Line 93 [Native to Managed Transition] ConsoleApplication2.exe!Dixin.Common.ExceptionFilter.D() Line 21 ConsoleApplication2.exe!Dixin.Common.ExceptionFilter.C() Line 12 ConsoleApplication2.exe!Dixin.Common.ExceptionFilter.B() Line 10 ConsoleApplication2.exe!Dixin.Common.ExceptionFilter.A() Line 8 ConsoleApplication2.exe!Dixin.Common.ExceptionFilter.Log() Line 91 ConsoleApplication2.exe!Dixin.Console.Program.Main() Line 110

Next, when the catch block is executed:

image

The current stack becomes:

ConsoleApplication2.exe!Dixin.Common.ExceptionFilter.Log() Line 95 ConsoleApplication2.exe!Dixin.Console.Program.Main() Line 110

This magic here is called stack unwinding: exception filter does not unwind the stack, and catch block does unwind. When executing catch block, this catch block’s method becomes the top frame of the stack. As a result, all the methods called by current method are removed from the stack. In contrast, exception filter can be helpful for runtime debugging. For example, if above Catch method is executed:

private static void Catch()
{
try
{
A();
}
catch (Exception exception)
{
exception.Log();
throw;
}
}

at runtime the debugger breaks at the throw statement in the catch block:

image

The stack is unwound. As a result the debugger cannot see the exception is actually thrown by D.

When executing the other When method:

private static void When()
{
try
{
A();
}
catch (Exception exception) when (exception.Log())
{
}
}

The Log method always returns false, so that the stack will not be unwound by catch block. This time the debugger breaks in method D, where the exception is actually thrown:

image

Notice in the Locals windows and Call Stack window, all information are available for debugger.

Conclusion#

C# 6.0 exception filter and when keyword is not a syntactic sugar. It is a CLR feature. Unlike catch block, exception filter does not unwind the call stack, which is helpful at runtime.

C# 6.0 Exception Filter and when Keyword
https://dixin.github.io/posts/c-6-0-exception-filter-and-when-keyword/
Author
Dixin
Published at
2016-03-07
License
CC BY-NC-SA 4.0