← Back to C# series
πŸ“‚
Exceptions & I/O
Exceptions/IO Β· Prerequisite: OOP

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.

C#.NET 8exception handling
Duration
⏱ ~1-1.5 hours
Level
πŸ“Š Intermediate
Prerequisite
🎯 OOP completed
OUTCOME
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`.

csharp
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
csharp
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**.

csharp
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.

csharp
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`.

csharp
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`.

csharp
using var sr = new StreamReader("a.txt");
// sr.Dispose() is auto-called when leaving this block

7) `ArgumentNullException.ThrowIfNull` (.NET 6+)

One-line argument validation.

csharp
public void Save(string name)
{
    ArgumentNullException.ThrowIfNull(name);   // throws immediately if null
    // ...
}

Examples

Example 1 β€” `TryCatch`: catching divide-by-zero

csharp
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**

text
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

csharp
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**

text
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`

csharp
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**

text
treat as a 404-like case

**Note:** If the `when` were false, the second `catch` would have caught it.

Example 4 β€” `CustomException`: custom exception

csharp
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**

text
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`

csharp
// 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**

text
> 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

csharp
PrintLength("hello");   // OK
PrintLength(null);      // ArgumentNullException

static void PrintLength(string? text)
{
    ArgumentNullException.ThrowIfNull(text);
    Console.WriteLine($"length: {text.Length}");
}

**Output**

text
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

xml
<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

csharp
// 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

xml
<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

csharp
// 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

csharp
// 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

xml
<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

csharp
// 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

xml
<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

csharp
// 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

xml
<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

csharp
// 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

xml
<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

  1. Only `catch (Exception)` and swallowing everything β€” you lose the root cause.
  2. Putting a narrower `catch` after a broader one β€” compile error (unreachable).
  3. Rethrowing with `throw ex;` β€” destroys the stack trace. Use **`throw;`**.
  4. Throwing again inside `finally` β€” masks the original exception.
  5. 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

text
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: 42

Hints

  • 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

text
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

xml
<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

csharp
// 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

xml
<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

csharp
// 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

bash
cd src/TryCatch && dotnet run
cd ../Finally && dotnet run
cd ../WhenFilter && dotnet run
cd ../CustomException && dotnet run
cd ../UsingDispose && dotnet run
cd ../ThrowIfNull && dotnet run

Next Lecture

[16_File_IO](../16_%ED%8C%8C%EC%9D%BC_IO/) β€” Safe patterns for opening and closing real files.

Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub β†—