C# (.NET 8 LTS) track

Lesson 15 · Part 4: Exceptions & I/O

Exception Handling

Handle failures with try, catch and finally, filter exceptions and throw meaningful errors.

Part 4: Exceptions & I/O5 min readFree lesson

Programs always live with the possibility of failure. Input that divides by zero, a missing file, a broken network — exception handling is a tool that allows the app to recover gracefully without dying** when these things happen. C# handles this with try/catch/finally and throw.

Learning Objectives

  • Understand the execution flow of try/catch/finally
  • Select and catch exceptions using multiple catch blocks and when clauses.
  • Create custom exception classes
  • Automatically releases resources with using
  • Use guard methods such as ArgumentNullException.ThrowIfNull

Key concepts

1) What is an exception?

It is an object representation of “unusual events” that occur at runtime. All exceptions inherit System.Exception .

try
{
    int x = int.Parse("abc");   //FormatException occurred
}
catch (FormatException ex)
{
    Console.WriteLine($"Invalid format: {ex.Message}");
}

If catch is not present, it will be thrown up the call stack, and if it is not caught until the end, the process will terminate.

2) try / catch / finally

  • try: wraps dangerous code
  • catch: Catch and handle exceptions (multiple per type possible)
  • finally: Always executed regardless of whether an exception occurs — Used to organize resources
try { /*work*/ }
catch (IOException ex) { /*File errors only*/ }
catch (Exception ex)   { /*everyone else*/ }
finally { /*organizing*/ }

Write the narrow type (catch) first and the wide type (Exception) later.

3) Condition filter with when clause

Attach a conditional expression to catch and capture it **only when certain conditions are met.

try { /* ... */ }
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    //404 only processed here
}

If the condition is false, the exception is thrown back up.

4) Throw exception with throw

You can create and throw an exception directly from an invalid argument or state.

if (age < 0)
    throw new ArgumentException("Age must be greater than or equal to 0.", nameof(age));

throw; (maintain stack trace) when you want to re-throw an exception that has already been caught. Avoid throw ex; because it initializes the stack trace.

5) Custom Exceptions

To preserve the domain meaning, inherit Exception.

public class InvalidAgeException : Exception
{
    public InvalidAgeException(string message) : base(message) { }
}

6) using and IDisposable

External resources such as files and networks implement IDisposable. Wrapping it in using will automatically call Dispose()** when it goes out of scope — no need to write finally yourself.

using var sr = new StreamReader("a.txt");
//Automatically calls sr.Dispose() when leaving this block

7) ArgumentNullException.ThrowIfNull (.NET 6+)

This is a one-line argument validation pattern.

public void Save(string name)
{
    ArgumentNullException.ThrowIfNull(name);   //If null, throw immediate exception
    // ...
}

See as example

Example 1 — TryCatch: Capturing division by 0

try
{
    int a = 10;
    int b = 0;
    int c = a / b;          // DivideByZeroException
    Console.WriteLine(c);
}
catch (DivideByZeroException ex)
{
    Console.WriteLine($"Cannot be divided by 0: {ex.Message}");
}

Console.WriteLine("Continue with the program");

Execution result

Attempted to divide by zero.
Continue with the program

Memo: Because the exception was caught, the following code runs normally.

Example 2 — Finally: finally runs unconditionally

try
{
    Console.WriteLine("try entry");
    throw new InvalidOperationException("thrown on purpose");
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"catch: {ex.Message}");
}
finally
{
    Console.WriteLine("finally: always executed");
}

Execution result

try entry
catch: intentionally thrown
finally: always executed

Note: Even if return or another exception occurs, finally is executed.

Example 3 — Filter messages with WhenFilter : when

try
{
    throw new InvalidOperationException("NotFound: No user found");
}
catch (InvalidOperationException ex) when (ex.Message.Contains("NotFound"))
{
    Console.WriteLine("Handles situations similar to 404");
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"Other errors: {ex.Message}");
}

Execution result

Handles situations similar to 404

Note: If condition when had been 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($"{age}year registration completed");
}

public class InvalidAgeException : Exception
{
    public InvalidAgeException(string message) : base(message) { }
}

Execution result

Registration failed: Age is negative: -1

Memo: Exception types that utilize domain meaning are easy to catch on the call side.

Example 5 — UsingDispose : Automatic release to using

//Demonstration using a memory reader instead of a file (actual files are covered in Part 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}");

//Automatically calling sr.Dispose() when scope ends

Execution result

> first line
> second line
> third line

Memo: using var is automatically released according to the variable scope (end of block).

Example 6 — ThrowIfNull: One-line guard

PrintLength("hello");   //normal
PrintLength(null);      // ArgumentNullException

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

Execution result

Length: 5
Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'text')

Note: Guard method of .NET 6+. The parameter name is automatically entered as Parameter 'text'.

Common Mistakes

  1. Burying all exceptions except catch (Exception) — The cause cannot be found.
  2. Placing the narrow type catch after the wide type — compile error (unreachable).
  3. Throw again with throw ex; — the stack trace disappears. Write throw;.
  4. finally throws another exception — the original exception is buried.
  5. Write exceptions to the normal flow — Exceptions are expensive. Consider patterns like TryParse first.

Cleanup

  • Separate dangerous codes and cleanup codes with try/catch/finally
  • You can start with the narrow type and set conditions with when.
  • Create domain exceptions yourself to make them more meaningful.
  • using and IDisposable resources are automatically released
  • Parameter verification in one line with ArgumentNullException.ThrowIfNull

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 run

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/CustomException/CustomException.csprojText
<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.csC#
//Custom exceptions with domain semantics
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($"{age}year registration completed");
}

public class InvalidAgeException : Exception
{
    public InvalidAgeException(string message) : base(message) { }
}
src/Finally/Finally.csprojText
<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.csC#
//finally is always executed regardless of exceptions.
try
{
    Console.WriteLine("try entry");
    throw new InvalidOperationException("thrown on purpose");
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"catch: {ex.Message}");
}
finally
{
    Console.WriteLine("finally: always executed");
}
src/ThrowIfNull/Program.csC#
//One-line guard methods in .NET 6+
try
{
    PrintLength("hello");           //normal
    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.csprojText
<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.csC#
//DivideByZeroException occurs if you divide by 0.
try
{
    int a = 10;
    int b = 0;
    int c = a / b;                  //exception occurs here
    Console.WriteLine(c);
}
catch (DivideByZeroException ex)
{
    Console.WriteLine($"Cannot be divided by 0: {ex.Message}");
}

Console.WriteLine("Continue with the program");
src/TryCatch/TryCatch.csprojText
<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.csC#
//using var: Automatically calls Dispose() when scope ends
//Demonstrate operation with a memory reader instead of a file (real files are covered in Part 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}");
}

//Here sr.Dispose() is automatically called
src/UsingDispose/UsingDispose.csprojText
<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.csC#
//The when clause only catches exceptions under certain conditions.
try
{
    throw new InvalidOperationException("NotFound: No user found");
}
catch (InvalidOperationException ex) when (ex.Message.Contains("NotFound"))
{
    Console.WriteLine("Handles situations similar to 404");
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"Other errors: {ex.Message}");
}
src/WhenFilter/WhenFilter.csprojText
<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>

Assignment

Problem 1 — Entering safe numbers

  • Reference solution: shown below
  • Key concepts: int.Parse/FormatException, try/catch, reentrant loop

Requirements

  • Displays “Enter an integer:” to the user and receives one line
  • If int.Parse fails, "Not an integer. Please enter again." and receive it again.
  • If successful, prints “Input value: NN” and exits.

Expected output

Please enter an integer: abc
It's not an integer. Please enter again.
Please enter an integer: 12.3
It's not an integer. Please enter again.
Please enter an integer: 42
Entered value: 42

Hint

  • Wrap it with try/catch (FormatException) within the while (true) loop.
  • If successful, break;
  • The return of Console.ReadLine() is string?, so it is received as ?? "" or processed separately.

Problem 2 — Insufficient Balance Exception

  • Reference solution: shown below
  • Core concepts: user-defined exceptions, throw, method guards

Requirements

  • Define InsufficientBalanceException : Exception
  • Create class BankAccount and place decimal Balance and void Withdraw(decimal amount)
  • Withdraw throws InsufficientBalanceException if amount is greater than Balance
  • Main attempts to withdraw 1500 from an account with a balance of 1000 and catches an exception and outputs a message.

Expected output

Withdrawal attempt: 1500
Failure: Insufficient balance (balance 1000, request 1500)

Hint

  • It is good for debugging if you include the balance and requested amount in the exception message.
  • Even if amount is 0 or less, let’s block it with ArgumentOutOfRangeException (optional)

Check the correct answer

After solving it yourself, compare it with the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

Homework01/Homework01.csprojText
<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>
Homework01/Program.csC#
//Integer input retry loop
while (true)
{
    Console.Write("Please enter an integer:");
    string input = Console.ReadLine() ?? "";

    try
    {
        int value = int.Parse(input);
        Console.WriteLine($"Entered value: {value}");
        break;
    }
    catch (FormatException)
    {
        Console.WriteLine("It's not an integer. Please enter again.");
    }
}
Homework02/Homework02.csprojText
<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>
Homework02/Program.csC#
//Insufficient balance exception scenario
var account = new BankAccount(1000m);

try
{
    Console.WriteLine("Withdrawal attempt: 1500");
    account.Withdraw(1500m);
}
catch (InsufficientBalanceException ex)
{
    Console.WriteLine($"Failure: {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), "The amount must be greater than 0.");

        if (amount > Balance)
            throw new InsufficientBalanceException($"insufficient balance (balance {Balance}, request {amount})");

        Balance -= amount;
    }
}

Learn this lesson with a mentor

Work through the C# (.NET 8 LTS) track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now