Laszlo

Hello, I am Laszlo

Software-Engineer, .NET developer

Contact Me

Try-Catch-Finally in C#

Introduction

C#'s try-catch-finally statements are fundamental building blocks of the language. While most developers learn exception handling early in their careers and use it frequently in production applications, the mechanics can be surprisingly complex. Exception handling is often discussed during developer job interviews, and even experienced developers can struggle with some of its nuances.

A Complex Case

Let's examine a challenging code snippet that demonstrates these complexities. What does the following code print to the console?

try
{
    try
    {
        throw new Exception2();
    }
    catch (Exception1) when (TestTrue())
    {
        Console.WriteLine("inner ex1");
        throw;
    }
    catch (Exception2) when (TestFalse())
    {
        Console.WriteLine("inner ex2");
        throw;
    }
    finally
    {
        Console.WriteLine("inner finally");
        throw new Exception1();
    }
}
catch (Exception2) when (TestFalse())
{
    Console.WriteLine("outer ex2");
}
catch (Exception1) when (TestTrue())
{
    Console.WriteLine("outer ex1");
}
finally
{
    Console.WriteLine("outer finally");
}


bool TestTrue()
{
    Console.WriteLine("test true");
    return true;
}

bool TestFalse()
{
    Console.WriteLine("test false");
    return false;
}

public class Exception2 : Exception1 { }
public class Exception1 : Exception { }

Understanding the Language Specification

To properly understand the behavior, we need to know three key concepts:

  1. How does C# match catch clauses for a given exception?
  2. When and how are finally blocks executed?
  3. How do exception filters (using the when keyword) work?

According to the .NET 10 and C# language specification (v8-draft):

"The search continues until a catch clause is found that can handle the current exception, by naming an exception class that is of the same class, or a base class, of the run-time type of the exception being thrown."

"Exception filters (when): The filter expression is evaluated before the stack is unwound. This means the original call stack and all local variables remain intact during filter evaluation."

"Once a matching catch clause is found, the system prepares to transfer control to the first statement of the catch clause. Before execution of the catch clause begins, the system first executes, in order, any finally clauses that were associated with try statements more nested than the one that caught the exception."

Understanding the Code Snippet Output

The code prints the following output on the console:

test true
inner ex1
test false
test true
inner finally
test true
outer ex1
outer finally

Let's review the execution step-by-step with the language reference in mind:

  1. An exception typed as Exception2 is thrown: throw new Exception2();.
  2. Exception1 is the base type of the thrown exception, hence the first inner catch clause matches it.
  3. The when clause is executed for the first inner catch clause, it prints test true.
  4. The exception filter also returns true, so the first inner catch block is executed, printing inner ex1.
  5. This catch block re-throws the exception while preserving the original stack trace.
  6. An enclosing catch clause is being searched for.
  7. The first outer catch block matches the type of the exception, it could handle the exception.
  8. The when clause of the first outer catch clause is executed, which prints test false to the console and returns false.
  9. The second outer catch clause matches the type of the exception, it could handle the exception.
  10. The when clause of the second outer catch clause is executed, which prints test true to the console and returns true.
  11. Before the execution of the second outer catch block begins, the inner finally block is executed, printing inner finally on the console.
  12. The inner finally block throws a new exception, typed Exception1.
  13. The second outer catch clause matches the type of this new exception.
  14. The when clause of the second outer catch clause is executed, which prints test true to the console and returns true.
  15. The second outer catch block is executed, printing outer ex1 on the console.
  16. The exception is handled by the block.
  17. The outer finally block is executed, printing outer finally on the console.

This example illustrates the complexity of exception handling, particularly when dealing with nested try-catch blocks, exception filters, and finally blocks that throw new exceptions.