Exceptions
When something goes wrong that a method can't handle — a file is missing, input is malformed, a network call fails — it throws an exception. The exception travels up the chain of calls until some code catches it, or the program stops. This lesson covers catching, throwing, cleaning up with finally, and writing your own exception types.
try and catch
Put code that might fail in a try block. If an exception is thrown, the rest of the block is skipped and the matching catch block runs:
string[] inputs = ["42", "oops", "7"]; foreach (string text in inputs) { try { int n = int.Parse(text); Console.WriteLine($"100 / {n} = {100 / n}"); } catch (FormatException ex) { Console.WriteLine($"'{text}' isn't a number: {ex.Message}"); } } Console.WriteLine("Still running.");
100 / 42 = 2 'oops' isn't a number: The input string 'oops' was not in a correct format. 100 / 7 = 14 Still running.
Without the try/catch, the second input would have crashed the program. The exception object (ex) carries a Message, the exception type, and a StackTrace showing where it happened.
Catching different exceptions
A try can have several catch blocks. The first one whose type matches wins, so list specific types before general ones:
foreach (string text in new[] { "5", "0", "abc", "99999999999" }) { Console.WriteLine(Divide(text)); } static string Divide(string text) { try { int n = int.Parse(text); return $"{1000 / n}"; } catch (DivideByZeroException) { return "can't divide by zero"; } catch (FormatException) { return "not a number"; } catch (Exception ex) { return $"unexpected {ex.GetType().Name}"; } }
200 can't divide by zero not a number unexpected OverflowException
All exceptions derive from System.Exception, so catch (Exception) catches everything. Use it sparingly — usually only at the top level of a program, to log the error — because it also catches bugs you'd rather find.
Exception filters
A when clause catches an exception only if a condition is true. Otherwise it keeps travelling up as if never caught:
foreach (int code in new[] { 404, 503, 500 }) { try { throw new HttpRequestException($"Server returned {code}", null, (System.Net.HttpStatusCode)code); } catch (HttpRequestException ex) when ((int?)ex.StatusCode == 404) { Console.WriteLine("Not found — showing a friendly page"); } catch (HttpRequestException ex) when ((int?)ex.StatusCode >= 500) { Console.WriteLine($"Server error {(int?)ex.StatusCode} — will retry"); } }
Not found — showing a friendly page Server error 503 — will retry Server error 500 — will retry
finally
A finally block always runs — whether the try succeeded, an exception was caught, or an exception is still on its way up. It's for cleanup that must happen no matter what:
Process(true); Process(false); static void Process(bool fail) { Console.WriteLine("Opening connection"); try { if (fail) throw new InvalidOperationException("Query failed"); Console.WriteLine("Query OK"); } catch (InvalidOperationException ex) { Console.WriteLine($"Error: {ex.Message}"); } finally { Console.WriteLine("Closing connection"); } }
Opening connection Error: Query failed Closing connection Opening connection Query OK Closing connection
For objects that need cleaning up, the using statement (next lesson) writes the finally for you.
Throwing exceptions
Throw an exception when your method is asked to do something it can't. Choose the most specific built-in type that fits:
| 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 the call makes sense |
NotSupportedException | The operation isn't supported at all |
KeyNotFoundException, FileNotFoundException | Something looked up doesn't exist |
var cart = new Cart(); try { cart.Add("Pen", 0); } catch (ArgumentOutOfRangeException ex) { Console.WriteLine(ex.Message); } try { cart.Checkout(); } catch (InvalidOperationException ex) { Console.WriteLine(ex.Message); } class Cart { private readonly List<string> _items = []; public void Add(string item, int quantity) { ArgumentException.ThrowIfNullOrWhiteSpace(item); ArgumentOutOfRangeException.ThrowIfLessThan(quantity, 1); for (int i = 0; i < quantity; i++) _items.Add(item); } public void Checkout() { if (_items.Count == 0) throw new InvalidOperationException("Cannot check out an empty cart."); } }
quantity ('0') must be greater than or equal to '1'. (Parameter 'quantity')
Actual value was 0.
Cannot check out an empty cart.The ThrowIf… helpers on the exception classes produce clear, consistent messages with the parameter name filled in.
Rethrowing
Sometimes you want to react to an exception — log it, say — but still let it continue upwards. Use throw; on its own, which preserves the original stack trace. (throw ex; would reset it, hiding where the error really started.) To add context, wrap it in a new exception and pass the original as the inner exception:
try { LoadSettings("settings.json"); } catch (InvalidOperationException ex) { Console.WriteLine(ex.Message); Console.WriteLine($" caused by {ex.InnerException?.GetType().Name}"); } static void LoadSettings(string path) { try { File.ReadAllText(path); } catch (FileNotFoundException ex) { Console.WriteLine("[log] settings file missing"); throw new InvalidOperationException($"Couldn't load settings from {path}.", ex); } }
[log] settings file missing Couldn't load settings from settings.json. caused by FileNotFoundException
Custom exceptions
Create your own exception class when callers need to catch a specific problem from your code and react to it differently. Derive from Exception and add any extra information as properties:
var account = new Account(50m); try { account.Withdraw(80m); } catch (InsufficientFundsException ex) { Console.WriteLine(ex.Message); Console.WriteLine($"Short by {ex.Shortfall:C}"); } class InsufficientFundsException(decimal balance, decimal requested) : Exception($"Cannot withdraw {requested:C}; balance is only {balance:C}.") { public decimal Shortfall { get; } = requested - balance; } class Account(decimal balance) { public void Withdraw(decimal amount) { if (amount > balance) throw new InsufficientFundsException(balance, amount); balance -= amount; } }
Cannot withdraw $80.00; balance is only $50.00. Short by $30.00
Name custom exceptions with the suffix Exception.
Exceptions are for exceptional cases
Throwing and catching is slow compared to ordinary code, and using exceptions for normal control flow makes programs hard to follow. When failure is an expected outcome — a user typing letters into a number field, a key that may not be in a dictionary — use the Try pattern (int.TryParse, TryGetValue) or return a result instead. Save exceptions for genuine errors.
Unhandled exceptions
If no code catches an exception, the program terminates and prints the exception type, message and stack trace. You've seen many examples in this course — every Unhandled exception. line came from one. In the full output the stack trace follows, showing the chain of method calls that led to the error, with file names and line numbers. It's the first thing to read when debugging (lesson 62).
Try it yourself
Parse each line as name:age, collecting errors instead of crashing. Add your own checks for empty names.
Ana is 34 Eve is 29 3 errors: 'Ben': expected name:age 'Cleo:abc': age must be a non-negative number 'Dev:-5': age must be a non-negative number