C# Try Catch Finally

try, catch, and finally let a C# program deal with unexpected runtime problems without crashing blindly. They matter because real programs parse input, call services, open files, talk to databases, and do many other operations that can fail even when the code is written correctly. Exception handling gives you a structured way to recover when recovery is possible, report useful information when it is not, and always clean up resources.

Overview: How Try, Catch, and Finally Work

An exception is an object that represents an error or unusual condition during program execution. In C#, most exceptions inherit from System.Exception. When code throws an exception, normal execution of the current statement stops. The Common Language Runtime, or CLR, starts looking up the call stack for a matching catch block that can handle the exception type.

A try block marks the section of code where an exception might occur. A catch block handles an exception thrown from the matching try. A finally block runs after the try and any selected catch, whether an exception happened or not. This makes finally useful for cleanup such as closing a file, releasing a lock, rolling back temporary state, or logging that a risky operation has ended.

Internally, exception handling is not a simple if statement. The compiler emits metadata describing protected regions of code and their handlers. While no exception is thrown, a try block usually runs with very little overhead. When an exception is thrown, however, the runtime must create or propagate an exception object, preserve stack information, search for a handler, and unwind stack frames. That is why exceptions are excellent for exceptional conditions, but poor for ordinary control flow in tight loops.

Exception matching is type based. A catch (FormatException) handles FormatException and derived types, but not DivideByZeroException. A catch (Exception) can handle almost any ordinary exception, so it must come after more specific catches. If no matching handler exists in the current method, the exception continues to the method that called it. If it reaches the application entry point without being handled, the program terminates and the runtime reports an unhandled exception.

Syntax

try
{
    // Code that might throw an exception.
}
catch (SpecificException ex)
{
    // Handle one known exception type.
}
catch (AnotherException ex) when (condition)
{
    // Handle only when the filter condition is true.
}
finally
{
    // Cleanup that runs whether an exception happened or not.
}
Part Purpose
try Contains code whose exceptions you want to handle in a controlled way.
catch Handles a matching exception type. You can have zero or more catches if a finally exists, but a plain try must have at least one catch or finally.
ex A variable that refers to the exception object. It contains information such as Message, StackTrace, and sometimes custom properties.
when An exception filter. The catch only runs if the filter expression evaluates to true.
finally Runs after the protected operation completes, even if the operation threw and even if a catch rethrows.

Examples

Example 1: Catching a Parsing Error

using System;

class Program
{
    static void Main()
    {
        string input = "not-a-number";

        try
        {
            int value = int.Parse(input);
            Console.WriteLine($"Parsed value: {value}");
        }
        catch (FormatException ex)
        {
            Console.WriteLine("The input was not a valid whole number.");
            Console.WriteLine($"Problem: {ex.Message}");
        }
        finally
        {
            Console.WriteLine("Parsing attempt finished.");
        }
    }
}

Output:

The input was not a valid whole number.
Problem: The input string 'not-a-number' was not in a correct format.
Parsing attempt finished.

int.Parse throws FormatException when the text cannot be converted to an integer. The first Console.WriteLine inside the try is skipped because execution jumps to the matching catch. The finally block still runs, so the program can reliably record that the attempt is over.

Example 2: Multiple Catch Blocks and Specific Handling

using System;

class Program
{
    static void Main()
    {
        string[] inputs = { "12", "0", "abc" };

        foreach (string input in inputs)
        {
            try
            {
                int divisor = int.Parse(input);
                int result = 120 / divisor;
                Console.WriteLine($"120 / {divisor} = {result}");
            }
            catch (DivideByZeroException)
            {
                Console.WriteLine("Cannot divide by zero.");
            }
            catch (FormatException)
            {
                Console.WriteLine($"'{input}' is not a number.");
            }
            finally
            {
                Console.WriteLine($"Finished input: {input}");
            }
        }
    }
}

Output:

120 / 12 = 10
Finished input: 12
Cannot divide by zero.
Finished input: 0
'abc' is not a number.
Finished input: abc

This program has two different failure modes. The value 0 parses successfully but fails during division, so DivideByZeroException is handled. The value abc fails during parsing, so FormatException is handled. The finally block runs once for each input, which is useful when each iteration owns work that must be completed or cleaned up.

Example 3: Finally Runs Even When a Method Returns

using System;

class Program
{
    static void Main()
    {
        Console.WriteLine(GetStatus("ready"));
        Console.WriteLine(GetStatus("broken"));
    }

    static string GetStatus(string mode)
    {
        try
        {
            Console.WriteLine($"Opening resource for {mode}.");

            if (mode == "broken")
            {
                throw new InvalidOperationException("The resource failed to start.");
            }

            return "Operation succeeded.";
        }
        catch (InvalidOperationException ex)
        {
            return $"Operation failed: {ex.Message}";
        }
        finally
        {
            Console.WriteLine($"Closing resource for {mode}.");
        }
    }
}

Output:

Opening resource for ready.
Closing resource for ready.
Operation succeeded.
Opening resource for broken.
Closing resource for broken.
Operation failed: The resource failed to start.

The return statement does not skip finally. For ready, the method prepares the return value, runs finally, and then returns. For broken, the exception is caught, the catch prepares a different return value, finally runs, and then the method returns that failure message.

How It Works Step by Step

  1. The CLR starts executing statements inside the try block normally.
  2. If no exception occurs, the catch blocks are skipped and finally runs if present.
  3. If an exception is thrown, the rest of the try block is abandoned.
  4. The runtime compares the thrown exception type with the catch clauses in source order.
  5. If a matching catch has a when filter, the filter is evaluated before the catch body runs.
  6. The selected catch handles the exception. If the catch throws another exception or uses throw;, propagation continues.
  7. Before control leaves the protected region, finally runs.

Stack unwinding is the process of leaving methods and scopes after an exception. During unwinding, finally blocks for abandoned scopes run in the correct order. This is why cleanup code belongs in finally or in types used by a using statement. In modern C#, using is often preferred for disposable resources because the compiler translates it into reliable cleanup logic similar to try/finally.

Common Mistakes

Putting a General Catch Before a Specific Catch

try
{
    int value = int.Parse("bad");
}
catch (Exception)
{
    Console.WriteLine("General problem.");
}
catch (FormatException)
{
    Console.WriteLine("Bad number format.");
}

This is wrong because Exception already catches FormatException. The specific handler is unreachable, and the compiler reports an error. Put specific exception types first and general exception types last.

try
{
    int value = int.Parse("bad");
}
catch (FormatException)
{
    Console.WriteLine("Bad number format.");
}
catch (Exception)
{
    Console.WriteLine("General problem.");
}

Swallowing Exceptions Without a Useful Action

try
{
    SaveImportantData();
}
catch (Exception)
{
}

An empty catch hides failures. The program continues as if everything worked, which can corrupt state or make debugging extremely difficult. If you catch an exception, do something meaningful: recover, retry, translate it into a clearer exception, log it, or show a helpful message. If you cannot handle it, let it propagate.

Throwing Away the Original Stack Trace

catch (Exception ex)
{
    throw ex;
}

Using throw ex; resets important stack trace information. Inside a catch block, use throw; to rethrow the same exception while preserving the original call path.

catch (Exception)
{
    throw;
}

Best Practices

  • Catch the most specific exception type you can handle correctly.
  • Do not catch Exception just to silence errors. A broad catch is appropriate at application boundaries, logging boundaries, or when you truly can recover from any expected failure.
  • Use finally for cleanup that must happen whether the operation succeeds or fails.
  • Prefer using or await using for objects that implement IDisposable or IAsyncDisposable.
  • Use throw;, not throw ex;, when rethrowing from a catch block.
  • Avoid exceptions for normal decisions such as checking whether user input is numeric. For those cases, prefer methods like int.TryParse.
  • Keep catch blocks small. The goal is to handle the failure, not hide a large second workflow inside the error path.
  • Include enough diagnostic information to debug the problem, but do not expose sensitive internal details to end users.

Practice Exercises

  1. Write a program that parses three hard-coded strings as integers. Print the parsed value when it works, and print a friendly message for invalid input. Add a finally message for each attempt.
  2. Create a method named SafeDivide that accepts two integers and returns a string. It should catch division by zero and return Cannot divide by zero.; otherwise return the quotient.
  3. Write a method that throws an InvalidOperationException for an invalid status value. Catch it in Main, print the message, and use finally to print Validation complete.

Summary

  • try marks code whose runtime failures you want to handle.
  • catch handles matching exception types in top-to-bottom order.
  • finally runs after the protected operation, whether it succeeded, failed, returned, or rethrew.
  • The CLR searches the call stack for a handler and runs cleanup during stack unwinding.
  • Good exception handling is specific, intentional, and diagnostic. It helps a program fail clearly or recover safely.