.NET Academy

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 the try block is skipped.
  • C# checks the catch blocks in order and runs the first one whose type matches.
  • Put specific exception types before general ones like Exception.
  • ex gives 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:

ExceptionThrow when…
ArgumentExceptionAn argument is invalid
ArgumentNullExceptionAn argument is null but mustn't be
ArgumentOutOfRangeExceptionAn argument is outside the allowed range
InvalidOperationExceptionThe object isn't in a state where this call makes sense
NotSupportedExceptionThe 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

Program.cs
Loading editor…

C# runtime not loaded

Key takeaways

  • try/catch handles exceptions; specific catch blocks go first; finally always runs.
  • Throw ArgumentException and friends to reject invalid input early — use nameof.
  • Rethrow with throw;, never throw 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.

Program.cs
Loading editor…

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 pass
  1. 1.When does a finally block run?

  2. 2.You have catch blocks for Exception and for FormatException. In which order must they appear?

  3. 3.Inside a catch block, what's the difference between `throw;` and `throw ex;`?

  4. 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)