Exception Handling
Things go wrong at runtime: a file isn't there, a divisor turns out to be zero, a user types letters into a number field. C# reports these failures as exceptions — objects that carry information about what went wrong — and gives you try/catch to intercept them before they crash your program.
try and catch
Code that might fail goes in a try block. If it throws an exception, execution jumps straight to a matching catch block instead of crashing the program:
int[] scores = { 88, 92, 79 };
try
{
Console.WriteLine(scores[5]);
}
catch (IndexOutOfRangeException e)
{
Console.WriteLine($"Couldn't read that score: {e.Message}");
}
Console.WriteLine("Program keeps running.");
Couldn't read that score: Index was outside the bounds of the array. Program keeps running.
scores[5] reaches past the end of a 3-element array, which throws an IndexOutOfRangeException. Without the try/catch, that exception would propagate all the way up and terminate the program. With it, the catch block handles the problem and execution continues normally on the next line.
Catching specific exception types
You can stack multiple catch blocks to handle different failure types differently. C# checks them in order and runs the first one that matches:
string input = "abc"; try { int number = int.Parse(input); int result = 100 / number; Console.WriteLine(result); } catch (FormatException) { Console.WriteLine("That wasn't a valid number."); } catch (DivideByZeroException) { Console.WriteLine("Can't divide by zero."); }
That wasn't a valid number.
int.Parse("abc") can't turn that string into a number, so it throws a FormatException and the matching catch runs — the division never even gets attempted. Order matters here too: catch blocks for more specific exception types should come before more general ones, since C# uses the first match it finds top to bottom.
finally: code that always runs
A finally block runs whether or not an exception was thrown — useful for cleanup work like closing a file or a database connection that has to happen either way:
try
{
Console.WriteLine("Opening connection...");
throw new InvalidOperationException("Connection refused");
}
catch (InvalidOperationException e)
{
Console.WriteLine($"Failed: {e.Message}");
}
finally
{
Console.WriteLine("Closing connection.");
}
Opening connection... Failed: Connection refused Closing connection.
throw raises an exception manually — here with a message describing what went wrong. finally's "Closing connection." prints regardless of whether the catch block ran at all; if you removed the throw entirely, finally would still execute right after the try block finished normally.
Custom exceptions
For failures specific to your own program's logic, you can define an exception type of your own by inheriting from Exception:
class InsufficientFundsException : Exception
{
public InsufficientFundsException(string message) : base(message)
{
}
}
void Withdraw(double balance, double amount)
{
if (amount > balance)
{
throw new InsufficientFundsException($"Can't withdraw {amount}, balance is only {balance}");
}
Console.WriteLine("Withdrawal successful.");
}
try
{
Withdraw(50, 200);
}
catch (InsufficientFundsException e)
{
Console.WriteLine($"Transaction failed: {e.Message}");
}
Transaction failed: Can't withdraw 200, balance is only 50
A custom exception is a normal class — it just inherits from Exception (or a more specific built-in exception type) so it can be thrown and caught the same way. This lets calling code catch InsufficientFundsException specifically, rather than something generic that could mean anything went wrong.
if check or a return value is almost always the better fit.