Try-Catch-Finally in C#09/20/2026 | 4 minutes to read | Open in Markdown
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:
- How does C# match
catchclauses for a given exception? - When and how are
finallyblocks executed? - How do exception filters (using the
whenkeyword) 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:
- An exception typed as
Exception2is thrown:throw new Exception2();. Exception1is the base type of the thrown exception, hence the first innercatchclause matches it.- The
whenclause is executed for the first innercatchclause, it printstest true. - The exception filter also returns
true, so the first innercatchblock is executed, printinginner ex1. - This
catchblock re-throws the exception while preserving the original stack trace. - An enclosing
catchclause is being searched for. - The first outer
catchblock matches the type of the exception, it could handle the exception. - The
whenclause of the first outercatchclause is executed, which printstest falseto the console and returnsfalse. - The second outer
catchclause matches the type of the exception, it could handle the exception. - The
whenclause of the second outercatchclause is executed, which printstest trueto the console and returnstrue. - Before the execution of the second outer
catchblock begins, the innerfinallyblock is executed, printinginner finallyon the console. - The inner
finallyblock throws a new exception, typedException1. - The second outer
catchclause matches the type of this new exception. - The
whenclause of the second outercatchclause is executed, which printstest trueto the console and returnstrue. - The second outer
catchblock is executed, printingouter ex1on the console. - The exception is handled by the block.
- The outer finally block is executed, printing
outer finallyon 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.