Lesson 137 min read

Exception Handling

Expect the unexpected — catch errors before they crash your app

Why Things Go Wrong

Programs crash. Files go missing. Users type "banana" when you asked for a number. The internet cuts out mid-download. A good developer doesn't just write code that works — they write code that fails gracefully.

In C#, when something goes wrong, the system throws an exception — it then walks up the call stack looking for a handler, like pulling the fire alarm. If nobody handles the alarm (catches the exception), the entire building evacuates (your program crashes). But if you set up a handler, you can deal with the problem calmly.

The three keywords:

  • try — "Here's some code that might fail."
  • catch — "If it does fail, here's what to do instead."
  • finally — "No matter what happened, always do this at the end."

Basic try/catch/finally

// Basic try/catch
try
{
Console.Write("Enter a number: ");
string input = "not a number"; // simulating bad input
int number = int.Parse(input); // this will throw!
Console.WriteLine($"You entered: {number}");
}
catch (FormatException ex)
{
Console.WriteLine($"That's not a number! Error: {ex.Message}");
}
catch (OverflowException)
{
Console.WriteLine("That number is too big or too small!");
}
catch (Exception ex) // catches anything else
{
Console.WriteLine($"Something went wrong: {ex.Message}");
}
finally
{
Console.WriteLine("This always runs — cleanup goes here.");
}
// Catching with 'when' filter
try
{
int[] arr = { 1, 2, 3 };
Console.WriteLine(arr[10]); // out of bounds!
}
catch (IndexOutOfRangeException ex) when (ex.Message.Contains("index"))
{
Console.WriteLine("Tried to access an invalid index!");
}
Output
Enter a number: That's not a number! Error: The input string 'not a number' was not in a correct format.
This always runs — cleanup goes here.
Tried to access an invalid index!

Common Exception Types

C# has many built-in exception types. Here are the ones you'll see most:

  • NullReferenceException — you tried to use something that's null (the #1 bug in C#!)
  • ArgumentException / ArgumentNullException — bad input passed to a method
  • InvalidOperationException — the object isn't in the right state for this operation
  • IndexOutOfRangeException — array/list index too big or negative
  • FormatException — string couldn't be parsed to a number
  • FileNotFoundException — the file doesn't exist
  • DivideByZeroException — math with zero denominator
  • KeyNotFoundException — dictionary key doesn't exist

All exceptions inherit from System.Exception. When you catch Exception, you catch everything — but it's better to catch specific types so you know exactly what went wrong.

Throwing Exceptions & Custom Exceptions

// Throwing exceptions — validate your inputs!
static void SetAge(int age)
{
if (age < 0)
throw new ArgumentException("Age cannot be negative!", nameof(age));
if (age > 150)
throw new ArgumentOutOfRangeException(nameof(age), "Nobody lives that long!");
Console.WriteLine($"Age set to {age}");
}
// Custom exception class
class InsufficientFundsException : Exception
{
public decimal Amount { get; }
public decimal Balance { get; }
public InsufficientFundsException(decimal amount, decimal balance)
: base($"Cannot withdraw {amount:C} — only {balance:C} available.")
{
Amount = amount;
Balance = balance;
}
}
static void Withdraw(decimal balance, decimal amount)
{
if (amount > balance)
throw new InsufficientFundsException(amount, balance);
Console.WriteLine($"Withdrew {amount:C}. Remaining: {balance - amount:C}");
}
// Using them
try { SetAge(25); } catch (Exception ex) { Console.WriteLine(ex.Message); }
try { SetAge(-5); } catch (Exception ex) { Console.WriteLine(ex.Message); }
try
{
Withdraw(100m, 250m);
}
catch (InsufficientFundsException ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine($" Tried: {ex.Amount:C}, Had: {ex.Balance:C}");
}
Output
Age set to 25
Age cannot be negative! (Parameter 'age')
Cannot withdraw $250.00 — only $100.00 available.
  Tried: $250.00, Had: $100.00

Real-World Pattern: TryParse vs Parse

// Instead of try/catch for parsing, use TryParse!
string[] inputs = { "42", "hello", "3.14", "99" };
foreach (string input in inputs)
{
// TryParse — no exceptions, just true/false
if (int.TryParse(input, out int result))
{
Console.WriteLine($" '{input}' → {result} (valid!)");
}
else
{
Console.WriteLine($" '{input}' → not a valid integer");
}
}
// Guard clauses — throw early, avoid deep nesting
static string ProcessOrder(string? customerId, int quantity)
{
// Validate first, fail fast
if (customerId is null)
throw new ArgumentNullException(nameof(customerId));
if (string.IsNullOrWhiteSpace(customerId))
throw new ArgumentException("Customer ID can't be empty", nameof(customerId));
if (quantity <= 0)
throw new ArgumentOutOfRangeException(nameof(quantity), "Must order at least 1");
// Happy path — no deep nesting!
return $"Order placed: {quantity} items for customer {customerId}";
}
try
{
Console.WriteLine(ProcessOrder("C-123", 5));
Console.WriteLine(ProcessOrder(null, 3));
}
catch (ArgumentNullException ex)
{
Console.WriteLine($"Missing: {ex.ParamName}");
}
Output
  '42' → 42 (valid!)
  'hello' → not a valid integer
  '3.14' → not a valid integer
  '99' → 99 (valid!)
Order placed: 5 items for customer C-123
Missing: customerId
Note: 🎯 Don't use exceptions for flow control! Exceptions are for truly exceptional situations — not for things you can predict. If a user might type a bad number, use TryParse (returns bool) instead of Parse + try/catch. Exceptions are slow; bool checks are fast.

Quick check

When does the 'finally' block run?

Continue reading

StackData Structure
LIFO — array & linked-list backed
Exception HandlingJava
try/catch, checked vs unchecked, and throws
Error HandlingPython
Catch mistakes before they crash your program
LINQFile I/O