S4.0 What This Teaches
This tutorial covers C# exception handling:
try / catch / finally blocks
- Exception types and the hierarchy
- Throwing exceptions with
throw
- Custom exception classes
- Exception filters with
when
- The
using statement for deterministic cleanup
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.
| Exception | Common cause |
ArgumentNullException | Null argument where not allowed |
ArgumentOutOfRangeException | Argument outside valid range |
InvalidOperationException | Method call invalid for current object state |
NullReferenceException | Dereferenced a null reference |
IndexOutOfRangeException | Array/list index out of bounds |
FormatException | Input string not in expected format |
IOException | File or network I/O failure |
KeyNotFoundException | Dictionary 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
| Term | Meaning |
| try | Block wrapping code that might throw |
| catch | Handles a specific exception type; multiple catches are checked in order |
| finally | Runs after try/catch regardless of outcome; used for cleanup |
| throw | Raises an exception; bare throw in catch re-throws preserving stack |
| when | Exception filter: condition on a catch clause |
| Exception.Message | Human-readable description of the error |
| IDisposable | Interface signaling the object holds resources; use with using |
| using | Ensures Dispose() is called; equivalent to try/finally Dispose |