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 12 of 19 min
Chapter 7 · Lesson 1

try/catch/finally & Exception Types

Exception handling is the mechanism that lets a program respond to runtime errors in a controlled way rather than crashing. In C#, every exception is an object that derives from the base class System.Exception.

The Exception Hierarchy

The hierarchy starts with System.Exception. Two broad sub-branches exist beneath it: SystemException (thrown by the runtime for things like NullReferenceException, IndexOutOfRangeException, and DivideByZeroException) and ApplicationException (the historical base for user-defined exceptions, though modern guidance recommends deriving directly from Exception). Knowing this tree helps you decide how specific your catch clauses need to be.

try / catch / finally

A try block wraps the code that might throw. One or more catch blocks follow, each targeting a specific exception type. A finally block, if present, always runs — whether the try succeeded, threw, or even if a catch re-threw. This makes finally ideal for releasing resources when you cannot use a using statement.

code
try { /* risky code */ }
catch (IOException ex) { /* handle I/O issues */ }
catch (Exception ex)   { /* handle everything else */ }
finally { /* always runs */ }

Catch Order Matters

C# evaluates catch blocks top to bottom and picks the first match. Because Exception is the base of all exceptions, placing it first would swallow every error before more specific handlers can run. Always order catch blocks from most-specific to least-specific.

The when Clause (Exception Filters)

C# 6 introduced exception filters: catch (Exception ex) when (condition). The filter expression is evaluated before the stack unwinds, which is a crucial difference from catching and re-throwing inside the block. If the filter returns false the runtime continues searching for a handler further up the call stack — the stack trace is fully preserved.

code
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    // only handles 404s
}

Rethrowing Correctly

Inside a catch block, throw; (bare throw) re-throws the original exception with its original stack trace intact. throw ex; creates a new exception from the catch variable, resetting the stack trace to the current line and hiding where the error really originated. Always use throw; unless you are wrapping the exception in a new one.

AggregateException

When multiple tasks fail concurrently (e.g., via Task.WhenAll), the runtime wraps all failures in an AggregateException. You can inspect InnerExceptions to see each individual failure, or call Flatten() to normalise nested aggregates, and Handle() to process each inner exception and mark it as handled.

Code Examples

Multiple Catch Blocks with when Clausecsharp
using System;

int[] numbers = { 10, 0, 5 };

for (int i = 0; i <= numbers.Length; i++) // intentionally goes out of range
{
    try
    {
        int divisor = numbers[i];
        int result  = 100 / divisor;
        Console.WriteLine(````````100 / ${divisor} = ${result}````````);
    }
    catch (DivideByZeroException ex)
    {
        Console.WriteLine(````````Caught divide-by-zero: ${ex.Message}````````);
    }
    catch (IndexOutOfRangeException ex) when (i >= numbers.Length)
    {
        // The 'when' filter only matches if the index is truly out of range
        Console.WriteLine(````````Filter matched – index ${i} is out of range.````````);
    }
    catch (Exception ex)
    {
        // Catch-all – least specific, must come last
        Console.WriteLine(````````Unexpected error: ${ex.Message}````````);
    }
    finally
    {
        Console.WriteLine(````````--- iteration ${i} complete ---````````);
    }
}

Catch blocks are evaluated top-to-bottom. The 'when' clause lets us inspect state before the stack unwinds. 'finally' always runs regardless of whether an exception occurred.

Rethrowing Properly with throw;csharp
using System;

void ProcessFile(string path)
{
    try
    {
        // Simulate a file operation
        if (string.IsNullOrEmpty(path))
            throw new ArgumentNullException(nameof(path), "Path must not be null or empty.");

        Console.WriteLine(````````Processing: ${path}````````);
        throw new InvalidOperationException("Simulated processing error.");
    }
    catch (ArgumentNullException)
    {
        // Handle argument problems here without re-throwing
        Console.WriteLine("Bad argument – handled locally.");
    }
    catch (Exception)
    {
        Console.WriteLine("Logging error, then re-throwing with original stack trace...");
        throw; // bare throw – stack trace is preserved
        // DO NOT write: throw ex;  <-- resets stack trace!
    }
}

try
{
    ProcessFile("report.csv");
}
catch (Exception ex)
{
    Console.WriteLine(````````Outer catch: ${ex.GetType().Name} – ${ex.Message}````````);
}

Using bare 'throw;' re-throws the exception preserving the original stack trace. 'throw ex;' would reset it to this line, making debugging harder.

AggregateException Handlingcsharp
using System;
using System.Threading.Tasks;

async Task RunAllAsync()
{
    Task t1 = Task.Run(() => throw new InvalidOperationException("Task 1 failed"));
    Task t2 = Task.Run(() => throw new ArgumentException("Task 2 failed"));
    Task t3 = Task.Run(async () =>
    {
        await Task.Delay(10);
        Console.WriteLine("Task 3 succeeded");
    });

    try
    {
        await Task.WhenAll(t1, t2, t3);
    }
    catch (Exception)
    {
        // Task.WhenAll wraps all failures in AggregateException
        // When awaited, the first inner exception is surfaced – retrieve all via .Exception
        AggregateException? agg = t1.Exception?.Flatten();

        // Use Handle() to process each inner exception
        Task.WhenAll(t1, t2).Exception?.Flatten().Handle(ex =>
        {
            Console.WriteLine(````````Inner: [${ex.GetType().Name}] ${ex.Message}````````);
            return true; // mark as handled
        });
    }
}

await RunAllAsync();

Task.WhenAll captures all failures into an AggregateException. Flatten() removes nested AggregateExceptions, and Handle() lets you inspect and mark each inner exception as handled.

Quick Quiz

1. Why must more-specific catch blocks appear before less-specific ones?

2. What is the key advantage of 'catch (Exception ex) when (condition)' over checking the condition inside the catch body?

3. What is wrong with writing 'throw ex;' instead of 'throw;' inside a catch block?

4. Which class wraps multiple exceptions thrown by concurrently running tasks?

Was this lesson helpful?

PreviousNext