Lesson 10 • Intermediate Track
Exception Handling
By the end of this lesson you'll be able to stop your C# programs crashing on bad input — catching errors with try/catch/finally, handling specific problems precisely, throwing your own exceptions, and knowing when to skip exceptions entirely with TryParse.
What You'll Learn
- Wrap risky code in try/catch and read ex.Message
- Use finally for cleanup that must always run
- Catch specific exceptions (and order them correctly)
- Throw your own errors with throw and re-throw with throw;
- Write a simple custom exception that carries extra data
- Use TryParse and using instead of exceptions where it fits
💡 Real-World Analogy
An exception is a circuit breaker in your house. When something goes wrong — a short circuit, too much load — the breaker trips instead of letting the whole house burn down. The try block is the wiring that might fault; the catch block is the breaker that trips and lets you respond calmly; the finally block is the safety routine that runs whether or not anything tripped. Without it, one bad value (a user typing "abc" where you expected a number) takes the entire program down.
📊 Common Exception Types
| Exception | Thrown when… | Typical cause |
|---|---|---|
| NullReferenceException | You use a variable that is null | obj.Name when obj is null |
| FormatException | Text can't convert to a number | int.Parse("abc") |
| IndexOutOfRangeException | You read past the end of an array | arr[10] in a size-3 array |
| DivideByZeroException | You divide an int by 0 | 10 / 0 |
| InvalidOperationException | An object is in the wrong state | list.First() on empty list |
| ArgumentException | A method gets a bad argument | you throw it on invalid input |
Every one of these inherits from Exception, which is why a single catch (Exception ex) can catch them all — but, as you'll see, catching the specific type is usually better.
1. try / catch / finally
An exception is C#'s way of saying "I can't do this" at runtime — like parsing the text "abc" as a number. Left alone, it crashes your program. You put the risky code inside a try block; if it throws, C# jumps to a matching catch block instead of crashing. The optional finally block runs no matter what — perfect for cleanup. Read this worked example, run it, then you'll write your own.
Worked example: try / catch / finally
Read every comment, run it, and watch the program survive a crash.
using System;
class Program
{
static void Main()
{
// A try block holds code that MIGHT fail (throw an exception).
// If it fails, C# jumps straight to a matching catch block —
// the rest of the try is skipped.
try
{
Console.WriteLine("Before the risky line");
int number = int.Parse("abc"); // 💥 "abc" isn't a number -> throws
Console.WriteLine($"Got: {number}"); // SKIPPED — never runs
}
cat
...Your turn. The program below would crash because "oops" isn't a number. Wrap it in a try/catch so it fails gracefully — fill in the blanks marked ___ using the hints.
🎯 Your turn: catch a crash
Fill in the ___ blanks to wrap the risky line and print ex.Message.
using System;
class Program
{
static void Main()
{
// 🎯 YOUR TURN — wrap the risky line so a bad value can't crash us.
string userInput = "oops"; // pretend the user typed this
// 1) Open a try block
___ // 👉 the keyword try
{
int age = int.Parse(userInput); // 💥 this throws
Console.WriteLine($"You are {age}");
}
// 2) Catch ANY exception into a variable called ex
ca
...2. Catching Specific Exceptions
Catching the broad Exception works, but it treats every problem the same. Usually you want to react differently to different errors — a bad index isn't the same as bad text. So you list several catch blocks, each for a specific type. The rule that trips everyone up: specific catches must come before the general one. C# checks them top to bottom and stops at the first match, so if catch (Exception) came first it would swallow everything and the compiler rejects the unreachable ones below it.
Worked example: specific before general
See how the right catch block is chosen by exception type.
using System;
class Program
{
static void Main()
{
try
{
int[] scores = { 90, 80 };
Console.WriteLine(scores[5]); // 💥 index 5 doesn't exist
}
// SPECIFIC catches come FIRST — most precise wins.
catch (IndexOutOfRangeException ex)
{
Console.WriteLine($"Bad index: {ex.Message}");
// Bad index: Index was outside the bounds of the array.
}
catch (FormatException ex)
{
...Now you try. Dividing an int by zero throws a specific exception. Catch exactly that type, then add a finally block that always runs.
🎯 Your turn: specific catch + finally
Catch the exact divide-by-zero error and add a finally block.
using System;
class Program
{
static void Main()
{
// 🎯 YOUR TURN — catch the EXACT error, then add a finally block.
try
{
int a = 10;
int b = 0;
int result = a / b; // 💥 dividing an int by zero throws
Console.WriteLine(result);
}
// 1) Catch the SPECIFIC exception thrown by 10 / 0
catch (___ ex) // 👉 DivideByZeroException
{
Console.WriteLine($"Maths er
...3. Throwing & Re-throwing
You don't only catch exceptions — you can throw them too. When a method is handed input it can't work with, the cleanest response is throw new ArgumentException("...") so the caller knows immediately. If you catch an exception just to log it but can't fully handle it, re-throw with a bare throw;. Crucially, throw; keeps the original stack trace (the breadcrumb trail showing where the error really started); writing throw ex; instead resets it and hides the true source.
Worked example: throw and throw; (re-throw)
See a method throw, and why throw; beats throw ex;.
using System;
class Program
{
// A method can THROW to signal "I can't do my job with this input".
static int GetAge(string text)
{
if (string.IsNullOrWhiteSpace(text))
throw new ArgumentException("Age cannot be blank.");
return int.Parse(text); // may throw FormatException too
}
static void Main()
{
try
{
int age = GetAge(""); // 💥 throws ArgumentException
Console.WriteLine($"Age: {age}")
...🔎 Deep Dive: Custom Exceptions
When none of the built-in types describe your problem well, make your own. A custom exception is just a class that inherits from Exception. The big win is that it can carry extra data — here, exactly how much money is missing — so the catch block can give a genuinely helpful message instead of a generic one.
Worked example: a custom exception
An InsufficientFundsException that carries the shortfall amount.
using System;
// A custom exception is just a class that inherits from Exception.
// It can carry EXTRA data so the caller knows what went wrong.
class InsufficientFundsException : Exception
{
public decimal Shortfall { get; }
public InsufficientFundsException(decimal shortfall)
: base($"You are {shortfall:C} short.") // sets ex.Message
{
Shortfall = shortfall;
}
}
class Program
{
static void Withdraw(decimal balance, decimal amount)
{
if (amo
...Name custom exceptions ending in Exception by convention, and only create them when a built-in type genuinely doesn't fit.
4. When NOT to Use Exceptions
Exceptions are for the unexpected. A user typing letters into an age box is completely expected, so reaching for try/catch there is the wrong tool — it's slower and noisier. Use TryParse, which returns true/false instead of throwing. Separately, when you open something that must be closed (files, streams, connections), wrap it in a using block: it calls Dispose() for you automatically, even if an exception is thrown — the tidy version of a try/finally cleanup.
Worked example: TryParse & using
Handle expected failures without throwing, and clean up safely.
using System;
class Program
{
static void Main()
{
// EXPECTED failures (bad user input) shouldn't use try-catch at all.
// TryParse returns true/false instead of throwing — fast and clean.
string input = "hello";
if (int.TryParse(input, out int number))
Console.WriteLine($"Parsed: {number}");
else
Console.WriteLine($"'{input}' is not a whole number"); // this runs
// 'using' guarantees cleanup (Dispose) even if a
...Common Errors (and the fix)
- "CS0160: A previous catch clause already catches all exceptions": you put
catch (Exception)before a more specific catch, making the lower one unreachable. Order specific → general. - Swallowing exceptions silently: an empty
catch { }hides the bug — the program limps on in a broken state. At minimum logex.Message; only catch what you can actually handle. - Catching
Exceptiontoo broadly: a blanket catch also hides errors you never meant to handle (typos, null bugs). Catch the specific type, and let truly unexpected ones surface. - Re-throwing with
throw ex;: this resets the stack trace so you lose where the error began. Use a barethrow;to preserve it. - Using exceptions for normal control flow: looping with
try/catchto test if input parses is slow and unreadable. Useint.TryParse(...)— that's what it's for.
Pro Tips
- 💡 Catch specific, throw specific. The more precise the type, the more precise the fix — and the fewer real bugs you accidentally hide.
- 💡 Re-throw with
throw;, neverthrow ex;— keep the original stack trace intact. - 💡 Prefer
TryParseoverParsefor anything a user typed; it's far faster than throwing and catching. - 💡 Use
usingfor anything disposable (files, streams) so cleanup happens even on error. - 💡
finallyalways runs — even if youreturnfrom inside thetry. Put guaranteed cleanup there.
📋 Quick Reference
| Task | Code | Notes |
|---|---|---|
| Catch any error | catch (Exception ex) | Put it last |
| Read the message | ex.Message | Human-readable text |
| Always run | finally { ... } | Cleanup |
| Throw an error | throw new ArgumentException("…") | Signal bad input |
| Re-throw | throw; | Keeps stack trace |
| Parse safely | int.TryParse(s, out int n) | true / false |
| Auto-cleanup | using (var f = …) { … } | Calls Dispose() |
Frequently Asked Questions
Q: Should I just wrap my whole program in one big try/catch?
No. A giant catch hides where the error came from and tempts you to ignore it. Catch close to the risky operation, catch the specific type, and only handle what you can actually recover from.
Q: When do I use TryParse instead of try/catch?
Whenever failure is expected — typically user input or file content. Throwing and catching is slow; TryParse just returns false. Save exceptions for genuinely unexpected situations.
Q: What's the difference between throw; and throw ex;?
throw; re-throws the current exception and preserves the original stack trace (where it really started). throw ex; throws it again but resets the trace to this line, hiding the source. Almost always use throw;.
Q: Does finally run even if there's a return in the try?
Yes. finally runs whether the try finishes normally, throws, or returns early. That guarantee is exactly why it's the right place for cleanup.
Mini-Challenge: Safe Divider
No blanks this time — just a brief and an outline. Combine everything: parse user input safely, then divide inside a try/catch so neither bad text nor a zero can crash you. Run it with "4", "0", and "abc" to check all three paths.
🎯 Mini-Challenge: safe divider
Use TryParse for the text, try/catch for the division by zero.
using System;
class Program
{
static void Main()
{
// 🎯 MINI-CHALLENGE: safe divider
// 1. You're given some user text in 'input' (try "0", "4", "abc").
// 2. SAFELY turn it into an int. If it isn't a number, print
// "Not a number" and stop.
// 3. Otherwise divide 100 by it inside a try/catch and print the result.
// If they entered 0, catch DivideByZeroException and print
// "Cannot divide by zero".
// Hint: int
...🎉 Lesson Complete
- ✅
tryholds risky code;catchhandles the error;finallyalways runs - ✅ Know the common types:
NullReference,Format,IndexOutOfRange,DivideByZero,InvalidOperation - ✅ Order catches specific → general (general last) or you'll hit CS0160
- ✅
throwto signal bad input; re-throw withthrow;to keep the stack trace - ✅ Custom exceptions inherit from
Exceptionand can carry extra context - ✅ Use
TryParsefor expected failures andusingfor guaranteed cleanup - ✅ Next lesson: LINQ — query, filter, and transform collections in one clean expression
Sign up for free to track which lessons you've completed and get learning reminders.