Exceptions
Signal and handle errors with throw, try, catch and finally — and know when not to.
13 min read
You will learn
- Catch exceptions with try/catch and clean up with finally
- Throw the right built-in exception for invalid arguments
- Create a custom exception type
- Decide between exceptions and the Try pattern
When things go wrong
Files go missing, networks drop, users type "abc" where a number belongs. When code hits a situation it can't handle, .NET throws an exception: it stops executing the current code and travels back up the call stack looking for someone who can catch it. If nobody does, the program crashes with the message and stack trace you learned to read in Reading errors and debugging.
try / catch
Wrap code that might fail in try, and handle failures in catch:
try
{
string text = File.ReadAllText("settings.json");
Console.WriteLine($"Loaded {text.Length} characters");
}
catch (FileNotFoundException)
{
Console.WriteLine("No settings file — using defaults");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine($"Permission denied: {ex.Message}");
}- As soon as an exception is thrown in
try, the rest of thetryblock is skipped. - C# checks the
catchblocks in order and runs the first one whose type matches. - Put specific exception types before general ones like
Exception. exgives you the exception object:ex.Message,ex.StackTrace,ex.InnerException.
finally: cleanup that always runs
var connection = OpenConnection();
try
{
connection.Send(data);
}
finally
{
connection.Close(); // runs whether Send succeeded, threw, or returned
}In practice, the using statement (Track 5) writes this try/finally for you.
Exception filters
Catch only when a condition holds:
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
Console.WriteLine("Not found");
}Throwing exceptions
Your own methods should throw when they're called incorrectly — fail fast instead of continuing with bad data:
void SetAge(int age)
{
if (age < 0 || age > 150)
throw new ArgumentOutOfRangeException(nameof(age), age, "Age must be 0–150");
...
}
void Register(string email)
{
ArgumentException.ThrowIfNullOrWhiteSpace(email); // built-in guard helper
...
}Use the right built-in type:
| Exception | Throw when… |
|---|---|
ArgumentException | An argument is invalid |
ArgumentNullException | An argument is null but mustn't be |
ArgumentOutOfRangeException | An argument is outside the allowed range |
InvalidOperationException | The object isn't in a state where this call makes sense |
NotSupportedException | The operation isn't supported at all |
nameof(age) produces the string "age", and keeps working if you rename the parameter.
Rethrowing
Sometimes you want to log an exception and let it continue upward:
catch (Exception ex)
{
logger.LogError(ex, "Import failed");
throw; // ✅ rethrows, keeping the original stack trace
// throw ex; // ❌ resets the stack trace to this line
}Custom exceptions
When callers need to react to a specific domain problem, define your own type:
class InsufficientFundsException : Exception
{
public decimal Shortfall { get; }
public InsufficientFundsException(decimal shortfall)
: base($"Insufficient funds: {shortfall:F2} short")
{
Shortfall = shortfall;
}
}By convention, the name ends with Exception.
Exceptions are for exceptional situations
Throwing is relatively expensive and makes control flow harder to follow. For expected outcomes, return a result instead:
// ❌ using exceptions for normal flow
try { var n = int.Parse(input); } catch (FormatException) { ... }
// ✅ the Try pattern
if (int.TryParse(input, out var n)) { ... } else { ... }Don't swallow exceptions
catch (Exception) { } with an empty body hides bugs: the program continues in a broken state and nobody knows why. Only catch what you can actually handle — and at least log the rest.
Try it
C# runtime not loaded
Key takeaways
try/catchhandles exceptions; specificcatchblocks go first;finallyalways runs.- Throw
ArgumentExceptionand friends to reject invalid input early — usenameof. - Rethrow with
throw;, neverthrow ex;. - Don't use exceptions for expected outcomes, and never swallow them silently.
Exercise
ParseAge returns the age for valid input, throws ArgumentException when the text isn't a number, and ArgumentOutOfRangeException when it's outside 0–150. SafeDivide returns the result as text, or "Cannot divide by zero". Withdraw throws InsufficientFundsException with the message "Cannot withdraw 50, balance is 10" when the amount exceeds the balance.
C# runtime not loaded
Hint 1
ParseAge: use int.TryParse — if it fails, throw new ArgumentException("Not a number", nameof(input)). Then check the range.
Hint 2
SafeDivide: wrap the division in try and catch (DivideByZeroException).
Hint 3
Withdraw: if (amount > balance) throw new InsufficientFundsException($"Cannot withdraw {amount}, balance is {balance}");
Show solution
Console.WriteLine(ParseAge("42"));
Console.WriteLine(SafeDivide(10, 2));
int ParseAge(string input)
{
if (!int.TryParse(input, out int age))
throw new ArgumentException($"'{input}' is not a number", nameof(input));
if (age < 0 || age > 150)
throw new ArgumentOutOfRangeException(nameof(input), age, "Age must be between 0 and 150");
return age;
}
string SafeDivide(int a, int b)
{
try
{
return (a / b).ToString();
}
catch (DivideByZeroException)
{
return "Cannot divide by zero";
}
}
void Withdraw(decimal balance, decimal amount)
{
if (amount > balance)
throw new InsufficientFundsException($"Cannot withdraw {amount}, balance is {balance}");
}
class InsufficientFundsException : Exception
{
public InsufficientFundsException(string message) : base(message) { }
}Quiz
4 questions · 3 correct to pass1.When does a finally block run?
2.You have catch blocks for Exception and for FormatException. In which order must they appear?
3.Inside a catch block, what's the difference between `throw;` and `throw ex;`?
4.Which situation is best handled with the Try pattern (bool + out) rather than exceptions?
To complete this lesson
- ○ Solve the exercise
- ○ Pass the quiz (3/4 correct)