Exception filters is a new C# 6.0 feature. Visual Basic.NET and F# have this functionality for a long time. That is because exception filtering was implemented in CIL but not in C#. Now, this technique available for us. That's how you can use it:
try
{
Method();
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 0x07)
{
// do exception handling logic
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 0x148)
{
// do exception handling logic
}
catch (Exception)
{
// log unhandled exception and rethrow
throw;
}
If Method throws an exception, catch block will first try to match type of the exception and then will evaluate when block. If an expression in when block is true, execution will go to the catch block. If not, the next catch block will be evaluated.
Internal Implementation
Let's see simple example:
try
{
}
catch (Exception ex) when (ex.Message == "test")
{
throw;
}
and IL code:
.try
{
IL_0000: leave.s IL_0025
} // end .try
filter
{
IL_0002: isinst [mscorlib]System.Exception
IL_0007: dup
IL_0008: brtrue.s IL_000e
IL_000a: pop
IL_000b: ldc.i4.0
IL_000c: br.s IL_0020
IL_000e: callvirt instance string [mscorlib]System.Exception::get_Message()
IL_0013: ldstr "test"
IL_0018: call bool [mscorlib]System.String::op_Equality(string, string)
IL_001d: ldc.i4.0
IL_001e: cgt.un
IL_0020: endfilter
} // end filter
{ // handler
IL_0022: pop
IL_0023: rethrow
} // end handler
as you can see, instead of a catch block, we have a filter block. First, there is a check if exception is of the specified type (IL_0002: isinst [mscorlib]System.Exception) and then we see check from the when block:
IL_000e: callvirt instance string [mscorlib]System.Exception::get_Message()
IL_0013: ldstr "test"
IL_0018: call bool [mscorlib]System.String::op_Equality(string, string)
and then handler block:
{ // handler
IL_0022: pop
IL_0023: rethrow
}
Exceptions In when Block
One question that you might have, what will happen if we get an exception in a when block?
Answer: whole catch block will be ignored and we will move to the next catch block (if defined).
Let's check out simple example:
static void Main(string[] args)
{
try
{
Method();
}
catch (Exception ex)
{
Console.WriteLine($"Catch Exception at root level: {ex.GetType().Name}" );
}
}
public static void Method()
{
try
{
Console.WriteLine("Throw OutOfMemoryException");
throw new OutOfMemoryException();
}
catch (OutOfMemoryException) when (CheckFirstCondition())
{
Console.WriteLine("Catch first OutOfMemoryException");
Console.WriteLine("Throw ArgumentOutOfRangeException");
throw new ArgumentOutOfRangeException();
}
catch (OutOfMemoryException) when (CheckSecondCondition())
{
Console.WriteLine("Catch second OutOfMemoryException");
throw;
}
catch (Exception ex)
{
Console.WriteLine($"Catch Exception in method. Type: {ex.GetType().Name}");
Console.WriteLine("Rethrow last exception");
throw;
}
}
private static bool CheckSecondCondition()
{
Console.WriteLine("Check second condition");
return false;
}
private static bool CheckFirstCondition()
{
Console.WriteLine("Check first condition");
Console.WriteLine("Throw InvalidOperationException");
throw new InvalidOperationException();
return true;
}
In console you will see following:
Throw OutOfMemoryException
Check first condition
Throw InvalidOperationException
Check second condition
Catch Exception in method. Type: OutOfMemoryException
Rethrow last exception
Catch Exception at root level: OutOfMemoryException
As we can see, InvalidOperationException has been ignored.
Video version
Guys from Webucator created nice video based on this article (check out their C# course):
https://www.youtube.com/watch?v=BDy83_6rLeU