C# Fundamentals: From Your First Program to Async and JSON · Modern C# in Practice: Generics, Async, Errors and JSON
Exceptions and Resource Cleanup with IDisposable
Throw and catch exceptions properly, create custom exception types, use filters and finally, and release resources deterministically with IDisposable and using.
What you'll build
A mini file-processing service: a custom ValidationException, a TempWorkspace that implements IDisposable and deletes its files on exit, and a processing loop where one bad record doesn't stop the rest.
The idea
Exceptions signal that an operation could not do its job. Use them for exceptional situations, not for ordinary control flow.
try/catch/finally:finallyalways runs, whether or not an exception occurred.- Catch specific types first, the general
Exceptionlast. Use filters to narrow further:catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound). throw;(without the exception) rethrows and keeps the original stack trace.throw ex;resets it, which loses information.- Wrap with context:
throw new MyException("...", innerException)keeps the cause inInnerException. - Validate arguments early:
ArgumentNullException.ThrowIfNull(x),ArgumentOutOfRangeException.ThrowIfNegative(n).
Resources (files, connections, handles) must be released promptly. Types that own them implement IDisposable with a Dispose() method. Use a using statement or declaration and the compiler calls Dispose() automatically, even when an exception is thrown:
using var stream = File.OpenRead(path);
The runtime's garbage collector reclaims memory but doesn't promptly release external resources, which is why using matters. For async cleanup there is IAsyncDisposable and await using.
The code
var records = new[] { "10", "20", "oops", "-5", "40" };
var accepted = new List<int>();
var problems = new List<string>();
using (var workspace = new TempWorkspace("import"))
{
foreach (var record in records)
{
try
{
accepted.Add(Parse(record));
workspace.Log($"ok {record}");
}
catch (ValidationException e) when (e.Field == "range")
{
problems.Add($"range problem: {e.Message}");
}
catch (ValidationException e)
{
problems.Add($"{e.Field} problem: {e.Message} (caused by {e.InnerException?.GetType().Name ?? "nothing"})");
}
finally
{
workspace.Log($"processed {record}"); // always runs
}
}
Console.WriteLine($"workspace holds {workspace.Entries} log entries");
} // Dispose() runs here, even if an exception had escaped
Console.WriteLine($"accepted: {string.Join(", ", accepted)}");
foreach (var problem in problems) Console.WriteLine(problem);
// Rethrow with `throw;` to keep the stack trace; wrap to add context
try
{
try { Parse("oops"); }
catch (ValidationException) { Console.WriteLine("logging, then rethrowing"); throw; }
}
catch (ValidationException e)
{
Console.WriteLine("outer caught: " + e.Message);
}
// Argument validation helpers
try { Describe(null!); }
catch (ArgumentNullException e) { Console.WriteLine("null argument: " + e.ParamName); }
static string Describe(string text)
{
ArgumentNullException.ThrowIfNull(text);
return text.ToUpper();
}
static int Parse(string text)
{
try
{
var value = int.Parse(text);
if (value < 0) throw new ValidationException("range", $"{value} must not be negative");
return value;
}
catch (FormatException e)
{
throw new ValidationException("format", $"'{text}' is not a number", e); // keep the cause
}
}
class ValidationException(string field, string message, Exception? inner = null) : Exception(message, inner)
{
public string Field { get; } = field;
}
class TempWorkspace : IDisposable
{
private readonly List<string> _log = new();
private readonly string _name;
public TempWorkspace(string name)
{
_name = name;
Console.WriteLine($"workspace '{_name}' opened");
}
public int Entries => _log.Count;
public void Log(string line) => _log.Add(line);
public void Dispose()
{
_log.Clear();
Console.WriteLine($"workspace '{_name}' cleaned up");
}
}
Output
workspace 'import' opened workspace holds 8 log entries workspace 'import' cleaned up accepted: 10, 20, 40 format problem: 'oops' is not a number (caused by FormatException) range problem: -5 must not be negative logging, then rethrowing outer caught: 'oops' is not a number null argument: text
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Catching
Exceptioneverywhere. Catch only what you can handle; let the rest bubble up to a central handler. - Using
throw ex;. It resets the stack trace. Usethrow;to rethrow. - Swallowing exceptions with an empty
catch. At minimum, log them. - Forgetting
usingfor disposable objects (HttpClientis the exception: reuse one instance, ideally throughIHttpClientFactory). - Throwing for expected results. Use
int.TryParseand similarTry...methods for normal "might fail" cases.
Key points
- Catch specific exceptions first; use
whenfilters;finallyalways runs. - Use
throw;to rethrow, and pass the original asinnerExceptionwhen wrapping. - Implement
IDisposablefor types that own resources, and useusingso cleanup is guaranteed. - Validate arguments with
ThrowIfNulland friends; preferTryParsefor expected failures.
Further reading: Exceptions and exception handling.
Practice questions
What is the difference between `throw;` and `throw ex;` in a catch block?
Answer
throw; rethrows the current exception and preserves its original stack trace. throw ex; throws the same object as if it originated at that line, resetting the stack trace and hiding where the error really happened.
What does the `using` statement guarantee?
Answer
That Dispose() is called on the object when the block (or enclosing scope for a using declaration) ends, whether it ends normally or because of an exception, so resources are released deterministically.
Open in the app to mark lessons complete, take notes and track your progress.