C# exceptions, finally, and using
Throw and catch a custom exception, filter by condition, and release resources reliably.
using System;
class InsufficientFundsException : Exception {
public int Missing { get; }
public InsufficientFundsException(int missing) : base("short by " + missing) {
Missing = missing;
}
}
class Connection : IDisposable {
public void Dispose() {
Console.WriteLine("connection closed");
}
}
class Program {
static int Withdraw(int balance, int amount) {
if (amount > balance) {
throw new InsufficientFundsException(amount - balance);
}
return balance - amount;
}
static void Main() {
try {
using (var connection = new Connection()) {
Console.WriteLine("working");
Withdraw(50, 80);
Console.WriteLine("not reached");
}
} catch (InsufficientFundsException e) when (e.Missing > 10) {
Console.WriteLine("caught: " + e.Message);
} finally {
Console.WriteLine("finally");
}
if (!int.TryParse("12x", out int value)) {
Console.WriteLine("TryParse failed without an exception");
}
}
}working connection closed caught: short by 30 finally TryParse failed without an exception
Explanation
An exception interrupts normal flow and is passed up the call stack until a matching catch block handles it. Define a custom exception by deriving from Exception and add the data the handler needs. A catch can add a when filter, so it handles the exception only if a condition is true. A finally block runs whether or not an exception occurred, which makes it the place for clean-up.
For resources that must be released, such as files and connections, a class implements IDisposable, and the using statement calls Dispose automatically at the end of its block, even when an exception is thrown. The order in the example is: the body runs, Dispose runs as the block is left, then the matching catch, then finally.
Explore the dry run
using System; class InsufficientFundsException : Exception { public int Missing { get; } public InsufficientFundsException(int missing) : base("short by " + missing) { Missing = missing; }} class Connection : IDisposable { public void Dispose() { Console.WriteLine("connection closed"); }} class Program { static int Withdraw(int balance, int amount) { if (amount > balance) { throw new InsufficientFundsException(amount - balance); } return balance - amount; } static void Main() { try { using (var connection = new Connection()) { Console.WriteLine("working"); Withdraw(50, 80); Console.WriteLine("not reached"); } } catch (InsufficientFundsException e) when (e.Missing > 10) { Console.WriteLine("caught: " + e.Message); } finally { Console.WriteLine("finally"); } if (!int.TryParse("12x", out int value)) { Console.WriteLine("TryParse failed without an exception"); } }}Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| The custom exception carries the missing amount and a readable message. | — | — | |
| The body of the using block starts. | — | working | |
| 80 is more than 50, so the exception is thrown. The next line is skipped. | Missing = 30 | working | |
| Leaving the using block calls Dispose before the catch runs. | Missing = 30 | working connection closed | |
| The filter is true because 30 > 10, so this handler runs. | Missing = 30 | working connection closed | |
| The message built by the exception. | Missing = 30 | working connection closed caught: short by 30 | |
| finally runs last. | Missing = 30 | working connection closed caught: short by 30 finally | |
| TryParse reports bad input with a boolean, which is better for expected failures. | Missing = 30 | working connection closed caught: short by 30 finally TryParse failed without an exception |
Cost and common mistakes
Normal flow costs nothing extra; throwing and catching an exception costs far more than a normal return.
- Do not catch Exception and ignore it. That hides bugs. Catch the specific types you can handle.
- Use throw; (not throw e;) to rethrow, so that the original stack trace is kept.
- Exceptions are slow compared with return values. Use TryParse-style methods for ordinary, frequent failures.
Try it yourself
What happens if the filter is e.Missing > 100?
Reveal answer and reasoning
The filter is false, so this catch does not run and the exception propagates to the caller. finally still runs.