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
- The CLR starts executing statements inside the
tryblock normally. - If no exception occurs, the
catchblocks are skipped andfinallyruns if present. - If an exception is thrown, the rest of the
tryblock is abandoned. - The runtime compares the thrown exception type with the
catchclauses in source order. - If a matching catch has a
whenfilter, the filter is evaluated before the catch body runs. - The selected catch handles the exception. If the catch throws another exception or uses
throw;, propagation continues. - Before control leaves the protected region,
finallyruns.
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
Exceptionjust to silence errors. A broad catch is appropriate at application boundaries, logging boundaries, or when you truly can recover from any expected failure. - Use
finallyfor cleanup that must happen whether the operation succeeds or fails. - Prefer
usingorawait usingfor objects that implementIDisposableorIAsyncDisposable. - Use
throw;, notthrow 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
- 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
finallymessage for each attempt. - Create a method named
SafeDividethat accepts two integers and returns a string. It should catch division by zero and returnCannot divide by zero.; otherwise return the quotient. - Write a method that throws an
InvalidOperationExceptionfor an invalid status value. Catch it inMain, print the message, and usefinallyto printValidation complete.
Summary
trymarks code whose runtime failures you want to handle.catchhandles matching exception types in top-to-bottom order.finallyruns 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.
