Exception Handling in C#
Using try/catch/finally correctly, and knowing when an exception is the right tool at all.
2 min read
An exception is C#'s mechanism for signaling that something went wrong in a way normal control flow can't express — a file that doesn't exist, a network call that failed, an invalid argument. try/catch lets you handle that without crashing the whole program.
try, catch, and finally
try
{
int result = 10 / int.Parse("0");
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Can't divide by zero: {ex.Message}");
}
catch (FormatException ex)
{
Console.WriteLine($"Invalid number format: {ex.Message}");
}
finally
{
Console.WriteLine("This always runs, error or not");
}Catch blocks are checked top to bottom, so order them from most specific to least specific — a general catch (Exception ex) placed first would swallow everything below it, since it matches any exception type. finally runs whether or not an exception was thrown, making it the right place for cleanup that must always happen (closing a file, releasing a lock).
Throwing your own exceptions
void SetAge(int age)
{
if (age < 0)
{
throw new ArgumentException("Age cannot be negative", nameof(age));
}
// ...
}Throw the most specific built-in exception type that fits (ArgumentException, ArgumentNullException, InvalidOperationException) rather than a bare Exception — it lets callers catch precisely the failure they know how to handle, and tells a reader immediately what went wrong.
using: automatic cleanup
Resources like files, database connections, and network streams implement IDisposable and need to be released deterministically. using guarantees that, even if an exception is thrown partway through:
using (var reader = new StreamReader("data.txt"))
{
string content = reader.ReadToEnd();
} // reader.Dispose() runs automatically here, exception or notModern C# also supports a shorter form that disposes at the end of the enclosing scope, rather than needing its own block:
using var reader = new StreamReader("data.txt");
string content = reader.ReadToEnd();Exceptions are for exceptional cases
The single biggest mistake with exceptions is reaching for them as ordinary control flow. Checking whether a key exists before reading it, or whether user input parses as a number, isn't "exceptional" — it's an expected, common case:
// Avoid: using an exception for something entirely expected
try
{
int value = int.Parse(userInput);
}
catch (FormatException)
{
value = 0;
}
// Prefer: TryParse for an expected failure path
if (!int.TryParse(userInput, out int value))
{
value = 0;
}Exceptions carry real runtime cost (capturing a stack trace isn't free) and make control flow harder to follow. Reserve try/catch for genuinely unexpected failures, and reach for the "try" pattern methods (TryParse, TryGetValue) for outcomes you already expect to happen sometimes.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.