15. Exception Handling
Exception handling is the mechanism for dealing safely with unexpected situations. Learn try/catch/finally, throw, custom exceptions, and the using statement to write robust code.
What you'll learn
- 1Understand the execution flow of `try`/`catch`/`finally`
- 2Pick specific exceptions with multiple `catch` blocks and `when` clauses
- 3Define custom exception classes
- 4Release resources automatically with `using`
- 5Use guard methods like `ArgumentNullException.ThrowIfNull`
Overview
Programs always live with the possibility of failure. Divide-by-zero input, missing files, broken networks β even when these happen, exception handling helps your app **not die and recover gracefully**. C# uses `try`/`catch`/`finally` and `throw`.
Core Concepts
1) What is an exception?
An "abnormal event" at runtime represented as an object. All exceptions inherit `System.Exception`.
try
{
int x = int.Parse("abc"); // throws FormatException
}
catch (FormatException ex)
{
Console.WriteLine($"Bad format: {ex.Message}");
}Without a `catch`, the exception propagates up the call stack; if nobody handles it, the process terminates.
2) `try` / `catch` / `finally`
- `try`: wraps risky code
- `catch`: handles an exception (can have multiple by type)
- `finally`: **always runs regardless of exceptions** β for resource cleanup
try { /* work */ }
catch (IOException ex) { /* file errors */ }
catch (Exception ex) { /* everything else */ }
finally { /* cleanup */ }Put the narrower `catch` first, the broadest (`Exception`) last.
3) Filter with `when` clause
Attach a condition to `catch` to only catch in **certain situations**.
try { /* ... */ }
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
// only 404 handled here
}If the condition is false, the exception is re-thrown up.
4) Throwing with `throw`
You can throw an exception yourself for bad arguments or bad state.
if (age < 0)
throw new ArgumentException("age must be >= 0.", nameof(age));To rethrow an already-caught exception, use **`throw;`** (preserves the stack trace). Avoid `throw ex;` which resets the stack trace.
5) Custom exceptions
To convey domain meaning, inherit `Exception`.
public class InvalidAgeException : Exception
{
public InvalidAgeException(string message) : base(message) { }
}6) `using` and `IDisposable`
External resources like files and sockets implement `IDisposable`. Wrap with `using` and **`Dispose()` is automatically called** when leaving the scope β no need for a manual `finally`.
using var sr = new StreamReader("a.txt");
// sr.Dispose() is auto-called when leaving this block7) `ArgumentNullException.ThrowIfNull` (.NET 6+)
One-line argument validation.
public void Save(string name)
{
ArgumentNullException.ThrowIfNull(name); // throws immediately if null
// ...
}Examples
Example 1 β `TryCatch`: catching divide-by-zero
try
{
int a = 10;
int b = 0;
int c = a / b; // DivideByZeroException
Console.WriteLine(c);
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Cannot divide by zero: {ex.Message}");
}
Console.WriteLine("Program continues");**Output**
Cannot divide by zero: Attempted to divide by zero.
Program continues**Note:** Because we caught the exception, the code below runs normally.
Example 2 β `Finally`: `finally` always runs
try
{
Console.WriteLine("entering try");
throw new InvalidOperationException("thrown on purpose");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"catch: {ex.Message}");
}
finally
{
Console.WriteLine("finally: always runs");
}**Output**
entering try
catch: thrown on purpose
finally: always runs**Note:** `finally` runs even on `return` or another exception.
Example 3 β `WhenFilter`: filter messages with `when`
try
{
throw new InvalidOperationException("NotFound: user missing");
}
catch (InvalidOperationException ex) when (ex.Message.Contains("NotFound"))
{
Console.WriteLine("treat as a 404-like case");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"other error: {ex.Message}");
}**Output**
treat as a 404-like case**Note:** If the `when` were false, the second `catch` would have caught it.
Example 4 β `CustomException`: custom exception
try
{
Register(-1);
}
catch (InvalidAgeException ex)
{
Console.WriteLine($"Registration failed: {ex.Message}");
}
static void Register(int age)
{
if (age < 0)
throw new InvalidAgeException($"age is negative: {age}");
Console.WriteLine($"Registered age {age}");
}
public class InvalidAgeException : Exception
{
public InvalidAgeException(string message) : base(message) { }
}**Output**
Registration failed: age is negative: -1**Note:** Domain-specific exception types are easier to catch on the calling side.
Example 5 β `UsingDispose`: auto release with `using`
// Demo with an in-memory reader (real files are covered in lecture 16)
using var sr = new System.IO.StringReader("first line\nsecond line\nthird line");
string? line;
while ((line = sr.ReadLine()) is not null)
Console.WriteLine($"> {line}");
// sr.Dispose() is auto-called when scope ends**Output**
> first line
> second line
> third line**Note:** `using var` auto-releases at the end of the variable's scope (end of block).
Example 6 β `ThrowIfNull`: one-line guard
PrintLength("hello"); // OK
PrintLength(null); // ArgumentNullException
static void PrintLength(string? text)
{
ArgumentNullException.ThrowIfNull(text);
Console.WriteLine($"length: {text.Length}");
}**Output**
length: 5
Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'text')**Note:** Guard method in .NET 6+. The parameter name automatically becomes `Parameter 'text'`.
Full example code (src/)
src/CustomException/CustomException.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.IoEx15</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/CustomException/Program.cs
// Custom exception carrying domain meaning
try
{
Register(-1);
}
catch (InvalidAgeException ex)
{
Console.WriteLine($"Registration failed: {ex.Message}");
}
static void Register(int age)
{
if (age < 0)
throw new InvalidAgeException($"age is negative: {age}");
Console.WriteLine($"Registered age {age}");
}
public class InvalidAgeException : Exception
{
public InvalidAgeException(string message) : base(message) { }
}
src/Finally/Finally.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.IoEx15</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/Finally/Program.cs
// finally always runs regardless of exceptions
try
{
Console.WriteLine("entering try");
throw new InvalidOperationException("thrown on purpose");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"catch: {ex.Message}");
}
finally
{
Console.WriteLine("finally: always runs");
}
src/ThrowIfNull/Program.cs
// One-line guard from .NET 6+
try
{
PrintLength("hello"); // OK
PrintLength(null); // ArgumentNullException
}
catch (ArgumentNullException ex)
{
Console.WriteLine($"caught: {ex.Message}");
}
static void PrintLength(string? text)
{
ArgumentNullException.ThrowIfNull(text);
Console.WriteLine($"length: {text.Length}");
}
src/ThrowIfNull/ThrowIfNull.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.IoEx15</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/TryCatch/Program.cs
// Dividing by 0 raises a DivideByZeroException
try
{
int a = 10;
int b = 0;
int c = a / b; // exception here
Console.WriteLine(c);
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Cannot divide by zero: {ex.Message}");
}
Console.WriteLine("Program continues");
src/TryCatch/TryCatch.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.IoEx15</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/UsingDispose/Program.cs
// using var: auto-call Dispose() when leaving scope
// Demo via an in-memory reader (real files come in lecture 16)
using var sr = new System.IO.StringReader("first line\nsecond line\nthird line");
string? line;
while ((line = sr.ReadLine()) is not null)
{
Console.WriteLine($"> {line}");
}
// sr.Dispose() is auto-called here
src/UsingDispose/UsingDispose.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.IoEx15</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/WhenFilter/Program.cs
// Use a when clause to catch only specific cases
try
{
throw new InvalidOperationException("NotFound: user missing");
}
catch (InvalidOperationException ex) when (ex.Message.Contains("NotFound"))
{
Console.WriteLine("treat as a 404-like case");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"other error: {ex.Message}");
}
src/WhenFilter/WhenFilter.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.IoEx15</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Common Mistakes
- Only `catch (Exception)` and swallowing everything β you lose the root cause.
- Putting a narrower `catch` after a broader one β compile error (unreachable).
- Rethrowing with `throw ex;` β destroys the stack trace. Use **`throw;`**.
- Throwing again inside `finally` β masks the original exception.
- Using exceptions for normal flow β they're expensive. Prefer patterns like `TryParse`.
Summary
- `try`/`catch`/`finally` separates risky code from cleanup
- Catch narrower types first; you can also add `when` conditions
- Build domain exceptions for clarity
- `using` auto-releases `IDisposable` resources
- `ArgumentNullException.ThrowIfNull` validates parameters in one line
Practice
**Practice - 15. Exception Handling**
Problem 1 β Safe integer input
- Project folder: `Homework01/`
- Key concepts: `int.Parse`/`FormatException`, `try`/`catch`, re-prompt loop
Requirements
- Prompt "Enter an integer: " and read one line.
- If `int.Parse` fails, print "Not an integer. Try again." and re-prompt.
- On success print "Value entered: NN" and exit.
Expected output
Enter an integer: abc
Not an integer. Try again.
Enter an integer: 12.3
Not an integer. Try again.
Enter an integer: 42
Value entered: 42Hints
- Wrap in `try`/`catch (FormatException)` inside a `while (true)` loop.
- `break;` on success.
- `Console.ReadLine()` returns `string?` β use `?? ""` or handle separately.
Problem 2 β Insufficient-balance exception
- Project folder: `Homework02/`
- Key concepts: custom exception, `throw`, method guard
Requirements
- Define `InsufficientBalanceException : Exception`.
- `class BankAccount` has `decimal Balance` and `void Withdraw(decimal amount)`.
- `Withdraw` throws `InsufficientBalanceException` when `amount` > `Balance`.
- In `Main`, attempt to withdraw 1500 from a 1000 account, catch the exception, and print the message.
Expected output
Withdraw attempt: 1500
Failed: insufficient funds (balance 1000, requested 1500)Hints
- Including balance and requested amount in the message aids debugging.
- Also guard against `amount` β€ 0 with `ArgumentOutOfRangeException` (optional).
Check your answer
Try it yourself, then compare against the [`answer/`](./answer/) folder.
Answer (answer/)
homework/answer/Homework01/Homework01.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.IoEx15</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework01/Program.cs
// Re-prompt loop for integer input
while (true)
{
Console.Write("Enter an integer: ");
string input = Console.ReadLine() ?? "";
try
{
int value = int.Parse(input);
Console.WriteLine($"Value entered: {value}");
break;
}
catch (FormatException)
{
Console.WriteLine("Not an integer. Try again.");
}
}
homework/answer/Homework02/Homework02.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.IoEx15</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework02/Program.cs
// Insufficient-balance scenario
var account = new BankAccount(1000m);
try
{
Console.WriteLine("Withdraw attempt: 1500");
account.Withdraw(1500m);
}
catch (InsufficientBalanceException ex)
{
Console.WriteLine($"Failed: {ex.Message}");
}
public class InsufficientBalanceException : Exception
{
public InsufficientBalanceException(string message) : base(message) { }
}
public class BankAccount
{
public decimal Balance { get; private set; }
public BankAccount(decimal initial)
{
Balance = initial;
}
public void Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount), "amount must be > 0.");
if (amount > Balance)
throw new InsufficientBalanceException($"insufficient funds (balance {Balance}, requested {amount})");
Balance -= amount;
}
}
Try It Yourself
cd src/TryCatch && dotnet run
cd ../Finally && dotnet run
cd ../WhenFilter && dotnet run
cd ../CustomException && dotnet run
cd ../UsingDispose && dotnet run
cd ../ThrowIfNull && dotnet runNext Lecture
[16_File_IO](../16_%ED%8C%8C%EC%9D%BC_IO/) β Safe patterns for opening and closing real files.
All lecture materials and example code are openly available on GitHub.
View on GitHub β