How Try-Catch-Finally Works in C

In C#, robust error handling relies on try, catch, and finally blocks to detect runtime errors, recover from unexpected failures, and safely release critical system resources. This article provides a straightforward breakdown of how each block operates individually, how they work together to control program execution during an error, and the core rules for implementing them effectively in your C# applications.

The try Block

The try block contains the code that could potentially throw an exception during execution. When code inside a try block executes normally without errors, program flow moves directly past any associated catch blocks and enters the finally block (if present). If an error occurs, execution inside the try block halts immediately at the line that caused the error, and the runtime searches for a matching catch block.

try
{
    int divisor = 0;
    int result = 10 / divisor; // Throws DivideByZeroException
}

The catch Block

A catch block defines the error-handling logic when a specific exception occurs within the try block. You can define multiple catch blocks to handle different types of exceptions differently. C# evaluates catch blocks from top to bottom, so more specific exceptions must appear before general exceptions like System.Exception.

C# also supports exception filters using the when keyword, allowing you to catch exceptions only under certain runtime conditions:

try
{
    string content = File.ReadAllText("data.txt");
}
catch (FileNotFoundException ex)
{
    Console.WriteLine($"File missing: {ex.Message}");
}
catch (IOException ex) when (ex.HResult == -2147024864)
{
    Console.WriteLine("File is locked by another process.");
}
catch (Exception ex)
{
    Console.WriteLine($"An unexpected error occurred: {ex.Message}");
}

If no matching catch block is found, the exception bubbles up the call stack to the calling method. If left unhandled, the application terminates.

The finally Block

The finally block contains code that always runs, regardless of whether an exception was thrown, caught, or completely unhandled. It executes even if the try or catch block contains a return, break, or continue statement.

Because of this guaranteed execution, finally blocks are primarily used for resource cleanup, such as closing file handles, database connections, or network sockets.

FileStream file = null;
try
{
    file = File.Open("log.txt", FileMode.Open);
    // Perform operations on the file
}
catch (IOException ex)
{
    Console.WriteLine($"IO Error: {ex.Message}");
}
finally
{
    if (file != null)
    {
        file.Close();
        Console.WriteLine("File stream successfully closed.");
    }
}

Note: For types that implement IDisposable, the C# using statement or using declaration provides a cleaner shorthand that translates into a try-finally block under the hood.

Execution Order Summary

  1. try begins: Code runs line-by-line until completion or an exception is thrown.
  2. Exception occurs: Execution immediately jumps to the first matching catch block.
  3. catch executes: The error is handled, logged, or re-thrown using the throw; statement.
  4. finally executes: Cleanup code runs regardless of whether an exception occurred or was handled.
  5. Program continues: Normal execution resumes after the entire structure, unless an unhandled exception or an explicit throw terminates the thread.