Skip to content

Intermediate · Exceptions, delegates, events and async · topic 26 of 43 · 30 min

Exceptions: try, catch, finally and custom exceptions

Explanation

An exception is an object that describes an error, such as a missing file or bad input. When code throws one, .NET skips the rest of the work until a matching catch block handles it; if nothing catches it, the program crashes. finally runs whether or not an error happened. You can throw your own exceptions, and you should rethrow with throw; (not throw ex;) to keep the original details.

What is an exception?

Things go wrong in real programs: the internet drops, a user types letters into an amount box, a file is missing. When .NET meets a problem it cannot handle in the normal flow, it creates an exception object and throws it. Throwing means: stop here and hand the problem upwards to whoever called this code, then their caller, and so on, until some code catches it.

Think of it like this: An exception is like a fire alarm in an office. When it rings, everyone stops normal work and follows the exit plan (the catch block). The fire marshal handles the problem. If no one has an exit plan, the building is evacuated: the program crashes.

try and catch

csharp
Console.Write("Amount: ");
try
{
    decimal amount = decimal.Parse(Console.ReadLine() ?? "");
    Console.WriteLine($"You entered {amount}");
}
catch (FormatException)
{
    Console.WriteLine("That is not a number.");
}
Code that might fail goes in try. If a FormatException happens, the catch block runs and the program continues.

Several catch blocks and the Exception object

csharp
try
{
    string text = File.ReadAllText("prices.txt");
    int first = int.Parse(text.Split(',')[0]);
    Console.WriteLine(100 / first);
}
catch (FileNotFoundException ex)
{
    Console.WriteLine($"Missing file: {ex.FileName}");
}
catch (DivideByZeroException)
{
    Console.WriteLine("The first price is zero.");
}
catch (Exception ex)
{
    Console.WriteLine($"Unexpected error: {ex.Message}");
    throw;   // let it continue upwards after logging
}
Order catches from most specific to most general. Exception, the base class of all exceptions, must come last (otherwise error CS0160).

Every exception has a Message (a human description), a StackTrace (the chain of method calls that led to it) and sometimes an InnerException (an earlier error that caused this one). Log all three when something unexpected happens.

finally: always runs

csharp
Console.WriteLine("Start");
try
{
    Console.WriteLine("Working");
    throw new InvalidOperationException("Printer jammed");
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"Caught: {ex.Message}");
}
finally
{
    Console.WriteLine("Cleanup");
}
Console.WriteLine("End");
Shows Start, Working, Caught: Printer jammed, Cleanup, End. finally is for cleanup that must happen no matter what.

For objects that implement IDisposable (files, connections), a using statement is the easy form of try/finally: it calls Dispose for you.

Throwing your own exceptions

csharp
static void Withdraw(decimal balance, decimal amount)
{
    ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
    if (amount > balance)
        throw new InsufficientFundsException(balance, amount);
}

public class InsufficientFundsException : Exception
{
    public decimal Balance { get; }
    public decimal Requested { get; }
    public InsufficientFundsException(decimal balance, decimal requested)
        : base($"Cannot withdraw {requested}; balance is only {balance}")
    {
        Balance = balance;
        Requested = requested;
    }
}
A custom exception inherits from Exception and passes a message to base. Helpers like ThrowIfNegativeOrZero (.NET 8) and ArgumentNullException.ThrowIfNull (.NET 6) check arguments in one line.

throw; versus throw ex;

Inside a catch block, throw; rethrows the same exception and keeps its original stack trace, so you can still see the line where it really started. throw ex; resets the stack trace to the catch block, hiding where the error came from. Always use throw; to rethrow. To add context, wrap it: throw new OrderException("Could not save order 1001", ex); which keeps the original as InnerException.

Exception filters

csharp
try { await client.GetStringAsync(url); }
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
    Console.WriteLine("Page not found");
}
when adds a condition; the catch runs only if it is true.

Good habits and common mistakes

  • Empty catch blocks (catch { }) hide bugs. At least log the error.
  • Catching Exception everywhere: catch only what you can handle; let the rest reach a top-level handler that logs it.
  • Using exceptions for normal decisions: checking input with TryParse is clearer and much faster than catching FormatException.
  • throw ex; loses the stack trace: use throw;.
  • Showing ex.StackTrace to end users: it can leak internal details. Show a friendly message and log the details.
ExceptionUsual cause
NullReferenceExceptionUsing a member of null
ArgumentException / ArgumentNullException / ArgumentOutOfRangeExceptionA method got a bad argument
FormatExceptionParsing text that is not in the right format
InvalidOperationExceptionThe object is in the wrong state for this call (e.g. First on empty)
KeyNotFoundExceptionDictionary key missing
IndexOutOfRangeExceptionArray index outside 0..Length-1
FileNotFoundException / IOExceptionFile problems
HttpRequestExceptionA web request failed

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Exceptions: try, catch, finally and custom exceptions · 1/7

Exceptions

In this lesson you will handle errors without crashing.

Prefer a video by a teacher? Find free videos on “C# exception handling try catch finally custom exception throw” on YouTube ↗

In real life: An ATM withdrawal with clear errors

An ATM must never crash halfway. It checks the amount, uses a custom exception for low balance so the screen can show a precise message, and always logs the attempt in finally, whether it worked or not.

csharp
decimal balance = 3000m;
foreach (decimal request in new[] { 2000m, 5000m, -100m })
{
    try
    {
        if (request <= 0) throw new ArgumentException("Amount must be positive");
        if (request > balance) throw new InsufficientFundsException(balance, request);
        balance -= request;
        Console.WriteLine($"Dispensed {request}. Balance {balance}");
    }
    catch (InsufficientFundsException ex) { Console.WriteLine(ex.Message); }
    catch (ArgumentException ex) { Console.WriteLine($"Invalid: {ex.Message}"); }
    finally { Console.WriteLine($"Logged request of {request}"); }
}

public class InsufficientFundsException(decimal balance, decimal requested)
    : Exception($"Cannot withdraw {requested}; balance is only {balance}") { }
Shows Dispensed 2000. Balance 1000, then Cannot withdraw 5000; balance is only 1000, then Invalid: Amount must be positive, each followed by its Logged line. The exception uses a C# 12 primary constructor to stay short.

Practice (0/4 done)

Task 1 · Question

What does this print? try { Console.Write("A"); throw new Exception(); } catch { Console.Write("B"); } finally { Console.Write("C"); }

Task 2 · Question

Why is throw; better than throw ex; inside a catch?

Task 3 · Question

Which order of catch blocks compiles?

Task 4 · Practice

Write a method ParseAge(string? input) that returns an int age. Throw ArgumentNullException if input is null, a custom InvalidAgeException if it is not a number or is outside 0 to 120. In the calling code, catch InvalidAgeException and print its message, and use finally to print "Validation finished".

Quick quiz

Question 1

1.When does a finally block run?

Question 2

2.What happens to an exception that nobody catches?

Question 3

3.A custom exception class should inherit from...

Question 4

4.What does when do in catch (IOException ex) when (...)?

Question 5

5.Which is the better way to handle a user typing "abc" for a number?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

When does a finally block run?

Answer: Always, whether or not an exception happened

finally is for guaranteed cleanup.

What happens to an exception that nobody catches?

Answer: The program crashes and shows the error

Unhandled exceptions end the process.

A custom exception class should inherit from...

Answer: Exception

Custom exceptions derive from Exception (or a more specific exception).

What does when do in catch (IOException ex) when (...)?

Answer: Adds a condition that must be true for the catch to run

It is an exception filter.

Which is the better way to handle a user typing "abc" for a number?

Answer: Use int.TryParse and check the result

Expected bad input is normal flow; TryParse avoids exceptions.

What happens to an exception nobody catches in main?Related

Answer: The thread ends and the stack trace is printed

Uncaught exceptions end the thread.

From Exceptions: try, catch and finally

null.length throws which error?Related

Answer: TypeError

Reading a property of null is a TypeError.

From Error handling: try, catch, finally and throw

Which base class should your own exception types usually derive from?Related

Answer: std::exception (often via std::runtime_error)

Then generic handlers for std::exception catch them too, and what() works.

From Exceptions: throw, try, catch and noexcept

When does the else block of a try statement run?Related

Answer: Only when the try block raised no exception

else runs only on success. finally is the block that runs every time.

From Errors and exceptions

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 4 more topics in “Exceptions, delegates, events and async” before its exam: Delegates, lambdas, Func, Action and Predicate, Events: letting objects notify each other, async and await: waiting without blocking, Files and JSON with System.Text.Json.
  2. Module 7 exam: Exceptions, delegates, events and async · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Web APIs with ASP.NET Core and EF Core. 43 topics left in the course before the final exam.

← Previous: LINQ in depth: grouping, joins, deferred execution, IQueryable