C#

C# Fundamentals

19 lessons

Progress0%
1. Introduction to C#
1What is C#?
2. Variables and Data Types
1Data Types in C#
3. Control Flow
ConditionalsLoops
4. Methods
Defining MethodsOptional Parameters and Overloading
5. Object-Oriented Programming
Classes and PropertiesInheritanceInterfaces and Generics
6. LINQ and Async
LINQ Queriesasync/await
7. Exception Handling
try/catch/finally & Exception TypesCustom Exceptions & IDisposable
8. Delegates & Events
Delegates & LambdaEvents & Event Handlers
9. Records & Pattern Matching
Record TypesPattern Matching & Switch Expressions
10. File I/O & JSON
File & Stream OperationsJSON Serialization
All Tutorials
C#Exception Handling
Lesson 13 of 19 min
Chapter 7 · Lesson 2

Custom Exceptions & IDisposable

Beyond catching built-in exceptions, well-designed libraries define their own exception types so callers can write precise catch clauses and handle domain-specific errors meaningfully.

Creating Custom Exception Classes

Derive from Exception (or a more specific subclass when appropriate). Convention demands you provide three constructors that mirror the standard exception constructors:

  1. MyException() — parameterless
  2. MyException(string message) — message only
  3. MyException(string message, Exception innerException) — message + cause

Adding [Serializable] and the protected serialisation constructor (SerializationInfo, StreamingContext) is legacy best practice; modern .NET code targeting .NET 6+ can skip the serialisation constructor unless cross-AppDomain remoting is required.

Exception Chaining with InnerException

When you catch a low-level exception and want to surface a higher-level one, pass the original as innerException. This chain is visible in ex.ToString() and lets debuggers trace the root cause without losing context.

code
catch (SqlException ex)
{
    throw new DataAccessException("Failed to load user", ex); // chained
}

IDisposable and the using Statement

The IDisposable interface exposes a single method Dispose(). Classes that hold unmanaged resources (file handles, database connections, network sockets) implement IDisposable so callers can release resources deterministically, without waiting for the garbage collector.

The using statement wraps an IDisposable object. When execution leaves the block — normally or via exception — Dispose() is called automatically, equivalent to a try/finally.

code
using (var conn = new SqlConnection(connectionString))
{
    conn.Open();
    // ... use conn ...
} // Dispose() called here

using Declaration (C# 8+)

C# 8 introduced the using declaration, which removes the nesting by tying the lifetime to the enclosing scope:

code
using var reader = new StreamReader(path);
// reader.Dispose() called when the enclosing scope exits

finally vs using

Both guarantee cleanup. Prefer using (or using declaration) for IDisposable objects — it is more concise and removes the boilerplate try/finally. Use a raw finally when you need to clean up something that is not IDisposable (e.g., resetting a flag or releasing a semaphore manually).

ExceptionDispatchInfo

System.Runtime.ExceptionServices.ExceptionDispatchInfo lets you capture an exception on one thread and re-throw it on another while preserving the original stack trace — useful in async infrastructure code and thread marshalling scenarios.

Code Examples

Custom Exception with InnerExceptioncsharp
using System;

// Custom exception following the standard three-constructor pattern
public class OrderProcessingException : Exception
{
    public int OrderId { get; }

    public OrderProcessingException() : base() { }

    public OrderProcessingException(string message) : base(message) { }

    public OrderProcessingException(string message, Exception innerException)
        : base(message, innerException) { }

    // Domain-specific constructor that also stores OrderId
    public OrderProcessingException(int orderId, string message, Exception innerException)
        : base(message, innerException)
    {
        OrderId = orderId;
    }
}

// --- usage ---
void ProcessOrder(int orderId)
{
    try
    {
        if (orderId <= 0)
            throw new ArgumentOutOfRangeException(nameof(orderId), "Order ID must be positive.");

        // Simulate a database failure
        throw new InvalidOperationException("DB connection lost.");
    }
    catch (Exception ex) when (ex is not OrderProcessingException)
    {
        // Wrap and chain the original exception
        throw new OrderProcessingException(orderId, ````````Failed to process order ${orderId}````````, ex);
    }
}

try
{
    ProcessOrder(42);
}
catch (OrderProcessingException ex)
{
    Console.WriteLine(````````OrderProcessingException: ${ex.Message}````````);
    Console.WriteLine(````````Order ID : ${ex.OrderId}````````);
    Console.WriteLine(````````Inner    : ${ex.InnerException?.Message}````````);
}

The custom exception stores domain context (OrderId) alongside the standard message. Exception chaining via InnerException preserves the root cause without losing it.

IDisposable Pattern Implementationcsharp
using System;

public class ManagedResource : IDisposable
{
    private bool _disposed = false;
    private readonly string _name;

    public ManagedResource(string name)
    {
        _name = name;
        Console.WriteLine(````````[${_name}] Acquired````````);
    }

    public void DoWork()
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        Console.WriteLine(````````[${_name}] Working...````````);
    }

    // Public Dispose — called by consumers
    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this); // tell GC finalizer is not needed
    }

    // Protected virtual so derived classes can extend cleanup
    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;

        if (disposing)
        {
            // Free managed resources
            Console.WriteLine(````````[${_name}] Managed resources released````````);
        }

        // Free unmanaged resources here (if any)
        Console.WriteLine(````````[${_name}] Disposed````````);
        _disposed = true;
    }
}

// The using statement guarantees Dispose() even if DoWork() throws
using (var res = new ManagedResource("FileHandle"))
{
    res.DoWork();
} // Dispose() called automatically here

Console.WriteLine("After using block");

The full IDisposable pattern separates managed and unmanaged cleanup, prevents double-disposal with '_disposed', and suppresses the finalizer when Dispose has already run.

using Declaration (C# 8+ Style)csharp
using System;
using System.IO;

void WriteAndRead(string path)
{
    // C# 8+ using declaration — no braces, scope = rest of method
    using var writer = new StreamWriter(path);
    writer.WriteLine("Line 1: Hello from using declaration");
    writer.WriteLine("Line 2: Disposed when method returns");
    // writer.Dispose() called here when WriteAndRead returns
}

void ReadFile(string path)
{
    using var reader = new StreamReader(path);

    string? line;
    int lineNum = 1;
    while ((line = reader.ReadLine()) != null)
    {
        Console.WriteLine(````````${lineNum++}: ${line}````````);
    }
    // reader.Dispose() called here
}

string tempFile = Path.GetTempFileName();
try
{
    WriteAndRead(tempFile);
    ReadFile(tempFile);
}
finally
{
    File.Delete(tempFile);
    Console.WriteLine("Temp file cleaned up.");
}

The 'using var' declaration (C# 8+) ties the lifetime of an IDisposable to the enclosing scope without adding an extra level of braces, making code flatter and easier to read.

Quick Quiz

1. When creating a custom exception class, which base class should you derive from in modern C#?

2. What does the 'using' statement guarantee even when an exception is thrown?

3. What is the purpose of calling GC.SuppressFinalize(this) inside Dispose()?

Was this lesson helpful?

PreviousNext