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
catchblocks andwhenclauses. - 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 codecatch: 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
- Burying all exceptions except
catch (Exception)— The cause cannot be found. - Placing the narrow type
catchafter the wide type — compile error (unreachable). - Throw again with
throw ex;— the stack trace disappears. Writethrow;. finallythrows another exception — the original exception is buried.- Write exceptions to the normal flow — Exceptions are expensive. Consider patterns like
TryParsefirst.
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.
usingandIDisposableresources 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