C# Exceptions

Handle run-time errors with try, catch and finally, catch specific exception types, throw your own exceptions with clear messages, create custom exception classes, use exception filters, and decide when exceptions are the right tool.

Lesson 17 of 43Intermediate17 min read

Even correct programs meet problems they can't control: a file has been deleted, the network drops, a user types letters into a number field, a payment provider is down. When .NET hits a problem it can't continue from, it throws an exception: an object describing what went wrong. If nothing catches it, the program stops and prints an error with a stack trace.

Exception handling lets you respond to these situations gracefully. You wrap risky code in a try block, handle specific problems in catch blocks, and put clean-up code that must always run in a finally block. Your own code can throw exceptions too, to refuse invalid input with a clear message instead of carrying on with bad data.

In this lesson you'll catch and handle exceptions, throw them with helpful messages, create custom exception types, and learn the professional rules: catch only what you can handle, never hide errors silently, and don't use exceptions for ordinary decisions.

try and catch

C#
try
{
    int number = int.Parse("abc");
    Console.WriteLine(number);
}
catch (FormatException ex)
{
    Console.WriteLine($"Not a number: {ex.Message}");
}
  • When an exception is thrown in try, the rest of the block is skipped and C# looks for a matching catch.
  • Catch specific types such as FormatException, FileNotFoundException or HttpRequestException. Put more specific catches before general ones, because the first match wins.
  • catch (Exception ex) catches everything. Use it only at the top level of an app, to log the error and show a friendly message.
  • The exception object has Message, StackTrace and InnerException properties.

finally and using

A finally block runs whether the try succeeded, failed, or returned early. It's for clean-up: closing files, releasing locks, stopping a timer.

C#
var timer = Stopwatch.StartNew();
try
{
    ProcessOrders();
}
finally
{
    Console.WriteLine($"Took {timer.ElapsedMilliseconds} ms");
}

For objects that implement IDisposable, such as files and database connections, the using statement is a shorter way to guarantee clean-up: using var reader = new StreamReader("data.txt");.

Throwing exceptions

Throw an exception when your code receives something it can't work with:

C#
void SetAge(int age)
{
    if (age < 0 || age > 150)
        throw new ArgumentOutOfRangeException(nameof(age), "Age must be between 0 and 150");
    // ...
}
  • ArgumentException, ArgumentNullException and ArgumentOutOfRangeException are for bad arguments. ArgumentNullException.ThrowIfNull(customer); is a one-line null check.
  • InvalidOperationException means the object isn't in a state where the operation makes sense, like shipping an order that's been cancelled.
  • To rethrow inside a catch block, write throw; on its own, which keeps the original stack trace.
  • Exception filters catch only when a condition is true: catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound).

C# Errors Syntax

Exception syntax

try { Risky(); }
catch (FileNotFoundException ex) { Console.WriteLine(ex.FileName); }
catch (IOException) { /* other I/O problems */ }
catch (Exception ex) when (ex.Message.Contains("timeout")) { }
finally { CleanUp(); }

throw new ArgumentException("Name is required", nameof(name));
ArgumentNullException.ThrowIfNull(order);
catch (Exception ex) { Log(ex); throw; }

class OutOfStockException(string product) : Exception($"{product} is out of stock");

C# Errors Summary

  • try runs risky code; catch handles specific exception types; the first matching catch wins.
  • finally always runs and is used for clean-up; using does the same for IDisposable objects.
  • throw new SomeException("message") reports a problem with a clear message.
  • throw; rethrows without losing the stack trace; when adds a filter to a catch.
  • Prefer TryParse-style methods for expected failures; never swallow exceptions silently.

C# Errors Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Handling different exceptions

C#
string[] inputs = { "10", "abc", "0", "99999999999" };

foreach (string input in inputs)
{
    try
    {
        int divisor = int.Parse(input);
        int result = 100 / divisor;
        Console.WriteLine($"100 / {divisor} = {result}");
    }
    catch (FormatException)
    {
        Console.WriteLine($"'{input}' is not a number");
    }
    catch (DivideByZeroException)
    {
        Console.WriteLine("Can't divide by zero");
    }
    catch (OverflowException)
    {
        Console.WriteLine($"{input} is too big for an int");
    }
}
Output
100 / 10 = 10
'abc' is not a number
Can't divide by zero
99999999999 is too big for an int
Line-by-line explanation
  1. Lines 7 to 8If either line throws, the rest of the try block is skipped.
  2. Line 11Each catch handles one specific kind of problem.
  3. Line 15Dividing an int by zero throws DivideByZeroException.
  4. Line 19int.Parse throws OverflowException for numbers outside the int range.
  5. Line 3The loop carries on with the next input after each handled exception.
Example 2

Throwing and a custom exception

C#
var stock = new Dictionary<string, int> { ["Lamp"] = 2, ["Mug"] = 0 };

void Reserve(string product, int quantity)
{
    ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity);
    if (!stock.TryGetValue(product, out int available))
        throw new KeyNotFoundException($"No product called {product}");
    if (available < quantity)
        throw new OutOfStockException(product, available);
    stock[product] = available - quantity;
}

foreach (var (item, qty) in new[] { ("Lamp", 1), ("Mug", 1), ("Desk", 1), ("Lamp", 0) })
{
    try
    {
        Reserve(item, qty);
        Console.WriteLine($"Reserved {qty} x {item}");
    }
    catch (OutOfStockException ex) when (ex.Available == 0)
    {
        Console.WriteLine($"{ex.Message}. We'll email you when it's back.");
    }
    catch (Exception ex) when (ex is KeyNotFoundException or ArgumentOutOfRangeException)
    {
        Console.WriteLine($"Request rejected: {ex.GetType().Name}");
    }
}

class OutOfStockException(string product, int available)
    : Exception($"{product} is out of stock")
{
    public int Available { get; } = available;
}
Output
Reserved 1 x Lamp
Mug is out of stock. We'll email you when it's back.
Request rejected: KeyNotFoundException
Request rejected: ArgumentOutOfRangeException
Line-by-line explanation
  1. Line 5ThrowIfNegativeOrZero is a one-line guard for invalid numbers.
  2. Line 7A clear message says exactly what was wrong.
  3. Line 9A custom exception carries extra information the caller can use.
  4. Line 20The when filter only catches out-of-stock errors where nothing is left.
  5. Line 24One catch handles two exception types with a pattern.
  6. Lines 30 to 34A custom exception inherits from Exception and passes its message to the base constructor.
Example 3

finally and rethrowing

C#
try
{
    ImportFile("orders.csv");
}
catch (Exception ex)
{
    Console.WriteLine($"Import failed: {ex.Message}");
}

void ImportFile(string path)
{
    Console.WriteLine("Opening connection");
    try
    {
        Console.WriteLine($"Reading {path}");
        throw new IOException($"{path} is locked by another program");
    }
    catch (IOException)
    {
        Console.WriteLine("Logging the I/O error");
        throw;
    }
    finally
    {
        Console.WriteLine("Closing connection");
    }
}
Output
Opening connection
Reading orders.csv
Logging the I/O error
Closing connection
Import failed: orders.csv is locked by another program
Line-by-line explanation
  1. Line 16Simulates a real I/O problem.
  2. Line 21throw; passes the same exception on to the caller after logging it.
  3. Lines 23 to 26finally runs before the exception leaves the method, so the connection always closes.
  4. Lines 5 to 8The top level catches the rethrown exception and shows its message.

Common C# Errors Mistakes

Swallowing exceptions

Don't
try { SaveOrder(order); }
catch { }                              // the order silently isn't saved
Do
try { SaveOrder(order); }
catch (DbUpdateException ex)
{
    logger.LogError(ex, "Saving order {Id} failed", order.Id);
    throw;
}

throw ex instead of throw

Don't
catch (Exception ex) { Log(ex); throw ex; }   // resets the stack trace
Do
catch (Exception ex) { Log(ex); throw; }

Using exceptions for normal checks

Don't
try { qty = int.Parse(text); } catch { qty = 1; }
Do
if (!int.TryParse(text, out qty)) qty = 1;

C# Errors in Real Projects

Exceptions in production systems

ASP.NET Core apps use middleware that catches any unhandled exception, logs it with full details, and returns a clean error response such as a ProblemDetails JSON object. Services throw domain exceptions like OrderNotFoundException that are translated into 404 responses. Resilience libraries such as Polly catch transient exceptions from networks and databases and retry automatically, while monitoring tools collect exceptions so developers see problems before users report them.

C# Errors FAQ

What is the difference between throw and throw ex in C#?

Inside a catch block, throw; rethrows the same exception and keeps its original stack trace, showing where the error really happened. throw ex; resets the stack trace to the current line, hiding the original location. Use throw;, or wrap it with throw new MyException("context", ex) to add information.

Does finally run if there is a return in try?

Yes. A finally block runs when the try block finishes normally, when it returns early and when an exception leaves it. The exceptions are extreme cases where the whole process stops, such as Environment.FailFast or a stack overflow.

How do I create a custom exception in C#?

Create a class that inherits from Exception and give it a name ending in Exception, such as class OutOfStockException : Exception. Add constructors that pass a message, and optionally an inner exception, to the base class. Only create one when callers need to catch that specific problem separately.

How do I catch multiple exception types in C#?

Write one catch block per type, with more specific types first, since the first matching block runs. If several types need the same handling, use one block with a filter: catch (Exception ex) when (ex is FormatException or OverflowException).

Should I catch Exception in C#?

Only in places where you can handle any failure sensibly, such as the top of a program, a request handler or a background job loop, where you log the error and carry on or show a friendly message. Elsewhere, catch the specific exceptions you expect, so that real bugs aren't silently hidden.

C# Errors Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the block that always runs, whether or not an exception happens.

try { Work(); }
catch (Exception) { Console.WriteLine("Failed"); }
 { Console.WriteLine("Done"); }
Predict the outputeasy

What does this print?

try
{
    Console.WriteLine("A");
    throw new Exception("x");
    Console.WriteLine("B");
}
catch { Console.WriteLine("C"); }
Multiple choicemedium

Why put specific catch blocks before catch (Exception)?

Multiple choicemedium

What's the difference between throw; and throw ex; in a catch block?

Multiple choicehard

When should you catch Exception, the base type of all exceptions?

C# Errors Quiz

5 questions to check what you learned.

  1. Which keyword reports an error from your own code?

    • catch
    • error
    • throw
    • raise
  2. Which exception does int.Parse("abc") throw?

    • NullReferenceException
    • FormatException
    • OverflowException
    • ArgumentException
  3. What does when add to a catch block?

    • A loop
    • A filter: the catch only handles the exception if the condition is true
    • A delay
    • Logging
  4. What should a custom exception class inherit from?

    • object
    • Error
    • Exception (or a more specific exception type)
    • IDisposable
  5. Which is better for checking whether user input is a valid number?

    • try int.Parse and catch FormatException
    • Convert.ToInt32 in a try block
    • A custom exception
    • int.TryParse