Site

Exceptions — C# try/catch/finally

Tutorial S4  •  C# / Learn

S4.0 What This Teaches

This tutorial covers C# exception handling: Rust uses Result<T, E> and ? instead of exceptions for recoverable errors. C# exceptions are for truly exceptional conditions; prefer TryParse/TryGet patterns for expected failures.

S4.1 try / catch / finally

try
{
    int x = int.Parse("abc");    // throws FormatException
    int y = 10 / 0;              // throws DivideByZeroException
}
catch (FormatException ex)
{
    Console.WriteLine($"Format error: {ex.Message}");
}
catch (DivideByZeroException)
{
    Console.WriteLine("Cannot divide by zero");
}
catch (Exception ex)            // catch-all for unexpected exceptions
{
    Console.WriteLine($"Unexpected: {ex.GetType().Name}");
}
finally
{
    Console.WriteLine("finally always runs");
}
finally runs whether or not an exception was thrown - even if a return is reached in the try block. Use it to release resources.

S4.2 Exception Hierarchy

All exceptions derive from System.Exception. Catch the most specific type first; catching Exception catches everything.
ExceptionCommon cause
ArgumentNullExceptionNull argument where not allowed
ArgumentOutOfRangeExceptionArgument outside valid range
InvalidOperationExceptionMethod call invalid for current object state
NullReferenceExceptionDereferenced a null reference
IndexOutOfRangeExceptionArray/list index out of bounds
FormatExceptionInput string not in expected format
IOExceptionFile or network I/O failure
KeyNotFoundExceptionDictionary key not found

S4.3 Throwing Exceptions

static double SquareRoot(double x)
{
    if (x < 0)
        throw new ArgumentOutOfRangeException(nameof(x), "Must be non-negative");
    return Math.Sqrt(x);
}

// Re-throw preserving stack trace
try { DoWork(); }
catch (Exception) { throw; }   // NOT throw ex; (which resets the stack trace)

S4.4 Custom Exceptions

class InsufficientFundsException : Exception
{
    public decimal Amount { get; }
    public decimal Balance { get; }

    public InsufficientFundsException(decimal amount, decimal balance)
        : base($"Cannot withdraw {amount:C}; balance is {balance:C}")
    {
        Amount = amount;
        Balance = balance;
    }
}

// Usage
try
{
    Withdraw(100m, balance: 50m);
}
catch (InsufficientFundsException ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine($"Short by {ex.Amount - ex.Balance:C}");
}

S4.5 Exception Filters

A when clause adds a condition to a catch block. The exception is only caught when the condition is true:
try
{
    int result = CallExternalApi();
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
    Console.WriteLine("Resource not found - returning default");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"HTTP error: {ex.StatusCode}");
    throw;
}

S4.6 using for Deterministic Cleanup

// using statement - Dispose() called even if exception is thrown
using (var reader = new StreamReader("data.txt"))
{
    string? line;
    while ((line = reader.ReadLine()) != null)
        Console.WriteLine(line);
}

// using declaration (C# 8+) - disposes at end of enclosing scope
using var writer = new StreamWriter("out.txt");
writer.WriteLine("Hello");
// writer.Dispose() called here automatically
Any type implementing IDisposable (file handles, database connections, network streams) should be wrapped in using.

S4.7 Example - All Together

// Exceptions - safe division and custom exception demo.

Console.WriteLine(SafeDivide(10, 3));    // 3.33
Console.WriteLine(SafeDivide(10, 0));    // fallback: 0
TryParseSafely("42");                    // parsed: 42
TryParseSafely("abc");                   // not a number: abc

static double SafeDivide(double a, double b)
{
    try { return a / b; }
    catch (DivideByZeroException) { return 0; }
}

static void TryParseSafely(string input)
{
    if (int.TryParse(input, out int n))
        Console.WriteLine($"parsed: {n}");
    else
        Console.WriteLine($"not a number: {input}");
}

S4.8 Exercise

Exercise
  • Write a method ParseAge(string input) that uses int.TryParse and throws ArgumentException if the result is negative or above 150.
  • Create a custom ValidationException with a FieldName property. Throw it from a method that validates a form-like data object.
  • Wrap a file-reading operation in try/catch/finally to handle FileNotFoundException and always print "done" from finally.

S4.9 Common Mistakes

Catching Exception too broadly

Catching Exception at every call site hides bugs. Catch only what you can meaningfully handle. Let unexpected exceptions propagate to an outer handler or crash the program with a useful stack trace.

throw ex instead of throw

catch (Exception ex) { throw ex; }  // resets stack trace - location is lost
catch (Exception)    { throw; }      // correct - stack trace preserved

Using exceptions for control flow

Exceptions are slow (stack unwinding, GC pressure). Use TryParse, TryGetValue, and similar methods for expected failures rather than catching exceptions in a loop.

S4.10 Key Terms

TermMeaning
tryBlock wrapping code that might throw
catchHandles a specific exception type; multiple catches are checked in order
finallyRuns after try/catch regardless of outcome; used for cleanup
throwRaises an exception; bare throw in catch re-throws preserving stack
whenException filter: condition on a catch clause
Exception.MessageHuman-readable description of the error
IDisposableInterface signaling the object holds resources; use with using
usingEnsures Dispose() is called; equivalent to try/finally Dispose