π Lesson 1.1: Exception Handling
Real programs meet real problems: missing files, bad input, dropped connections. In this lesson you'll learn to catch these errors and respond gracefully β instead of letting your program crash.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain what an exception is and when one is thrown
- Handle errors with
try,catch, andfinally - Catch specific exception types in the right order
- Throw exceptions with
throw, including your own custom types - Follow best practices for robust error handling
Estimated Time: 60 minutes
Project: Make a fragile program resilient with proper exception handling.
In This Lesson
What Is an Exception?
An exception is C#'s way of signaling that something went wrong at runtime β a problem the compiler couldn't catch, but that stops normal execution. You met one in the intro course: calling int.Parse("hello") throws a FormatException and crashes the program.
π Definition
Exception: an object representing an error that occurs while a program runs. When code throws an exception, normal flow stops and C# looks for code that will handle it.
When an exception is thrown and nothing handles it, it travels up the call stack until it reaches the top and terminates the program with an error message. Exception handling lets you intercept that and decide what to do instead.
π‘ Exceptions vs. the TryParse pattern
In the intro course you used int.TryParse to avoid a crash. That's ideal for expected situations like user typos. Exceptions are for exceptional problems β a missing file, a failed network call β where a crash would otherwise be the only outcome. This lesson gives you the general tool.
try and catch
You wrap risky code in a try block. If it throws, control jumps to the matching catch block instead of crashing:
try
{
Console.Write("Enter a number: ");
int number = int.Parse(Console.ReadLine());
Console.WriteLine($"You entered {number}.");
}
catch (Exception ex)
{
Console.WriteLine($"Something went wrong: {ex.Message}");
}
Console.WriteLine("The program continues normally.");
If the user types abc, int.Parse throws. Instead of crashing, the catch runs and the program keeps going:
Sample run (user types "abc"):
Enter a number: abc
Something went wrong: The input string 'abc' was not in a correct format.
The program continues normally.
The catch (Exception ex) captures the exception object in ex. Its most useful member is ex.Message, a human-readable description of what happened.
thrown?"} C -->|"No"| D["Skip catch"] C -->|"Yes"| E["Jump to catch block"] D --> F["Continue program"] E --> F style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style F fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px
Catching Specific Exceptions
Catching the broad Exception type works, but it treats every problem the same way. Usually you want to react differently to different failures. C# lets you write multiple catch blocks, each for a specific exception type:
try
{
int[] numbers = { 1, 2, 3 };
Console.Write("Enter an index: ");
int i = int.Parse(Console.ReadLine());
Console.WriteLine($"Value: {numbers[i]}");
}
catch (FormatException)
{
Console.WriteLine("That wasn't a valid whole number.");
}
catch (IndexOutOfRangeException)
{
Console.WriteLine("That index is outside the array.");
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex.Message}");
}
C# checks the catch blocks top to bottom and runs the first one whose type matches the thrown exception.
β οΈ Order matters: specific before general
Because the first match wins, list specific exception types before the general Exception. If catch (Exception) came first, it would catch everything and the specific blocks below it would be unreachable β the compiler will actually flag this as an error.
Here are some exception types you'll meet often:
| Exception | Typically thrown when⦠|
|---|---|
FormatException | Parsing text that isn't in the expected format |
IndexOutOfRangeException | Using an array index that doesn't exist |
NullReferenceException | Using a reference that is null |
FileNotFoundException | Opening a file that isn't there |
DivideByZeroException | Dividing an integer by zero |
ArgumentException | A method receives an invalid argument |
The finally Block
A finally block runs no matter what β whether the try succeeded, threw an exception, or the exception was caught. It's the place for cleanup that must always happen, like closing a file or releasing a resource:
try
{
Console.WriteLine("Opening resource...");
int result = 10 / int.Parse("0"); // throws DivideByZeroException
Console.WriteLine(result); // skipped
}
catch (DivideByZeroException)
{
Console.WriteLine("Cannot divide by zero.");
}
finally
{
Console.WriteLine("Cleaning up (always runs).");
}
Output:
Opening resource...
Cannot divide by zero.
Cleaning up (always runs).
π‘ using is often better for cleanup
For objects that implement IDisposable (files, connections), C#'s using statement automatically disposes them even if an exception occurs β a cleaner alternative to a manual finally. You'll use using in Module 4 when working with files.
Throwing Exceptions
You can raise exceptions yourself with the throw keyword. This is how a method signals that it was given something it can't work with β pushing the problem to the caller rather than silently continuing with bad data:
decimal Withdraw(decimal balance, decimal amount)
{
if (amount <= 0)
{
throw new ArgumentException("Amount must be positive.");
}
if (amount > balance)
{
throw new InvalidOperationException("Insufficient funds.");
}
return balance - amount;
}
The caller decides how to respond by wrapping the call in try/catch:
try
{
decimal newBalance = Withdraw(100m, 250m);
Console.WriteLine($"New balance: {newBalance:C}");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Transaction declined: {ex.Message}");
}
Output:
Transaction declined: Insufficient funds.
β Throw early, throw clearly
Validate inputs at the start of a method and throw immediately with a clear message if something's wrong. This "fail fast" habit keeps bad data from spreading deeper into your program where it's harder to diagnose.
Custom Exceptions
Sometimes the built-in exception types don't describe your problem well. You can define your own by creating a class that derives from Exception β a direct use of the inheritance you learned in the intro course:
class InsufficientFundsException : Exception
{
public InsufficientFundsException(string message) : base(message)
{
}
}
Now you can throw and catch it by name, making your intent crystal clear:
decimal Withdraw(decimal balance, decimal amount)
{
if (amount > balance)
{
throw new InsufficientFundsException(
$"Tried to withdraw {amount:C} but only {balance:C} available.");
}
return balance - amount;
}
try
{
Withdraw(100m, 250m);
}
catch (InsufficientFundsException ex)
{
Console.WriteLine($"Declined: {ex.Message}");
}
π‘ Why bother?
A custom exception type lets callers catch exactly the error they care about, and the type name itself documents what went wrong. By convention, custom exception class names end in Exception, and they pass their message up to the base with : base(message).
Best Practices
β Do
- Catch specific exceptions you can actually handle, not just
Exception. - Use the message. Log or display
ex.Messageso problems are diagnosable. - Clean up with
finallyor (better)using. - Validate and throw early with clear messages.
β Don't
- Don't swallow exceptions silently. An empty
catch { }hides bugs and makes failures invisible β one of the most damaging habits in programming. - Don't use exceptions for normal flow. For expected cases (like validating user input), prefer checks such as
TryParse; exceptions are comparatively expensive and obscure intent. - Don't catch what you can't handle. If a block can't do anything useful about an error, let it propagate to code that can.
π‘ The mindset: Exception handling isn't about making errors disappear β it's about responding to them deliberately. A good handler either fixes the situation, informs the user, or logs the problem and re-raises it.
Exercise & Quiz
ποΈ Exercise: A Safe Division Tool
Objective: Use multiple catch blocks, finally, and a custom exception.
Instructions:
- Create a new project called
SafeDivide. - Ask the user for two integers and divide the first by the second.
- Wrap it in
try/catchhandlingFormatException(bad input) andDivideByZeroException(dividing by zero) with distinct messages. - Add a
finallyblock that prints "Calculation attempt finished." - Bonus: Write a method
int Divide(int a, int b)thatthrows anArgumentExceptionifbis zero, and call it inside the try.
Starter Code:
try
{
Console.Write("Numerator: ");
int a = int.Parse(Console.ReadLine());
Console.Write("Denominator: ");
int b = int.Parse(Console.ReadLine());
Console.WriteLine($"Result: {a / b}");
}
// TODO: catch FormatException
// TODO: catch DivideByZeroException
// TODO: finally block
π‘ Hint
List the specific catches (FormatException, DivideByZeroException) before any general catch (Exception). The finally block goes last and always runs. For the bonus, throw new ArgumentException("...") inside Divide when b == 0.
β Solution
try
{
Console.Write("Numerator: ");
int a = int.Parse(Console.ReadLine());
Console.Write("Denominator: ");
int b = int.Parse(Console.ReadLine());
Console.WriteLine($"Result: {Divide(a, b)}");
}
catch (FormatException)
{
Console.WriteLine("Please enter valid whole numbers.");
}
catch (ArgumentException ex)
{
Console.WriteLine($"Bad argument: {ex.Message}");
}
catch (DivideByZeroException)
{
Console.WriteLine("You can't divide by zero.");
}
finally
{
Console.WriteLine("Calculation attempt finished.");
}
int Divide(int a, int b)
{
if (b == 0)
{
throw new ArgumentException("Denominator cannot be zero.");
}
return a / b;
}
Sample run:
Numerator: 10
Denominator: 0
Bad argument: Denominator cannot be zero.
Calculation attempt finished.
π― Quick Quiz
Question 1: What happens to code after the failing line inside a try block once an exception is thrown?
Question 2: Why must specific catch blocks come before catch (Exception)?
Question 3: When does a finally block run?
Summary
π Key Takeaways
- An exception signals a runtime error; unhandled, it crashes the program.
- Wrap risky code in
tryand handle failures incatch; readex.Messagefor details. - Catch specific exception types before the general
Exceptionβ order matters. finallyalways runs (great for cleanup);usingis often cleaner for disposable resources.- Use
throwto raise exceptions (fail fast), and derive fromExceptionfor custom types. Never silently swallow exceptions.
π Additional Resources
- Exceptions and exception handling β Microsoft Docs
- Best practices for exceptions
- try / catch / finally β reference
π What's Next?
You can now keep programs running through errors. Next, we make code reusable and type-safe at the same time. In Lesson 1.2: Generics, you'll write classes and methods that work with any type β the technology behind List<T> itself.
π Robustness unlocked!
Your programs no longer fall over at the first surprise. Next up: generics.