Unexpected exception filter behavior in C # 6

Original author: Rahul
  • Transfer

What are exception filters?


Exception Filters - a new feature of C # 6, which allows you to set specific conditions for the block catch. This block will be executed only if the specified conditions are met. We illustrate the syntax with a small piece of code:

public void Main()
{
  try
  { 
    throw new Exception("E2");
  }
  catch(Exception ex) when(ex.Message == "E1")
  {
    Console.WriteLine("caught E1");
  }
  catch(Exception ex) when(ex.Message == "E2")
  {
    Console.WriteLine("caught E2");
  }
}

Is this really a new feature?


For C #, yes. However, support for exception filters has long been present in IL and VB.NET. Even the F # language supports these filters using a mechanism called exception pattern matching.

But can we get this functionality using ordinary conditional statements?


Logically - yes, but there is a fundamental difference. If the condition is inside the catch-block, then the exception will be caught first, and then the condition will be checked. Exception filters check the condition before catching the exception. If the condition is not met, then the catch-block will be skipped, .NET will proceed to consider the next catch-block.

So what's the difference?


When you catch an exception, you have an unwound stack, i.e. losing important exception information. There is a misconception that if we execute throwinside the catch-block instead throw ex, we will save the stack. The fact is that people only think about the exception property StackTrace, but not about the CLR stack itself. Let's look at an example. If you try to run it yourself, make sure that the “Break on Exception” checkbox for catching exceptions is disabled in the Visual Studio settings (Debug-> Exceptions-> Uncheck all).

Consider the general scenario in which we catch an exception, log it, and do nothing else. The following image shows where the debugger stops when an exception is thrown. Pay attention to the stop line of the debugger and the Locals window:



Now let's rewrite the example a bit using exception filters. We make sure that the method Logalways returns falseand performs logging using exception filters instead of putting it inside the catch-block. Pay attention to the debugger stop line and the debugger window again ( Note: the post and example were updated, but the picture remains the same; you catch (if(Log()))should read it insteadcatch when (Log()) ):



From the translator: the original picture is outdated, since in the recent past the syntax of exception filters has changed a bit: earlier they were described using a keyword if, and now it has been replaced with a new keyword when. Motivation is well illustrated by the following picture:



In addition to information about exactly where the exception occurred, you can also see in the Locals window of the second example a local variable localVariablethat is not available in the first example, catchsince it is not in the stack inside the block. This corresponds to what we can see in crash dumps.

In addition, if you entered some catchblock, you won’t enter the others. If you have analyzed the condition and decided not to throw it away again, then you will no longer be able to get into other catchblocks. In the case of exception filters, an unfulfilled condition will not prevent us from checking the remaining conditions catchto try to enter another block.

Expected Behavior


Thus, we can specify the condition in the exception filter; catch-block will be executed only if the condition is met. And we can use the bool-function as a condition. But what happens if the condition itself throws an exception? The expected behavior is as follows: the exception is ignored, the conditions are considered false. Consider the following code:

class Program
{
    public static void Main()
    {
        TestExceptionFilters();
    }
    public static void TestExceptionFilters()
    {
        try
        {
            throw new Exception("Original Exception");
        }
        catch (Exception ex) when (MyCondition())
        {
            Console.WriteLine(ex);
        }
    }
    public static bool MyCondition()
    {
        throw new Exception("Condition Exception");
    }
}

At the moment of throwing an exception "Original Exception"before entering the catch-block, the condition will be checked MyCondition. But this condition itself throws an exception, which should be ignored, and the condition should be considered false. So we get an unhandled exception:

System.Exception: Original Exception

Unexpected behavior


The time has come for a strange example. We modify the above code so that instead of calling the method directly TestExceptionFilters(), this method will be called through reflection. The expected behavior remains the same, although we call the function differently.

class Program
{
    public static void Main()
    {
        var targetMethod = typeof(Program).GetMethod("TestExceptionFilters");
        targetMethod.Invoke(null, new object[0]);
    }
    public static void TestExceptionFilters()
    {
    try
    {
        throw new Exception("Original exception");
    }
    catch (Exception ex) when (MyCondition())
    {
        Console.WriteLine(ex);
    }
    }
    public static bool MyCondition()
    {
        throw new Exception("Condition Exception");
    }
}

Let's run this code. As expected, we get an unhandled exception, but only the type of exception will be different:

System.Exception: Condition Exception

Thus, the type of exception depends on the way we invoked the function. About this bug, an issue was launched on GitHub ( Exception filters have different behavior when invoked via reflection and the filter throws an exception ). At the time of writing, the bug is still present in CoreCLR. Hopefully someone will correct him soon.

From the translator: this post is a composite translation of two posts at once from the site www.volatileread.com : Unpredictable Behavior With C # 6 Exception Filters and C # 6 Exception Filters and How they are much more than Syntactic Sugar

Also popular now: