Exception Handling

Reviewed & published by Brayan K

By the end of this lesson you'll be able to stop your C# programs crashing on bad input — catching errors with try/catch/finally, handling specific problems precisely, throwing your own exceptions, and knowing when to skip exceptions entirely with TryParse.

Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

An exception is a circuit breaker in your house. When something goes wrong — a short circuit, too much load — the breaker trips instead of letting the whole house burn down. The try block is the wiring that might fault; the catch block is the breaker that trips and lets you respond calmly; the finally block is the safety routine that runs whether or not anything tripped. Without it, one bad value (a user typing "abc" where you expected a number) takes the entire program down.

📊 Common Exception Types

ExceptionThrown when…Typical cause
NullReferenceExceptionYou use a variable that is nullobj.Name when obj is null
FormatExceptionText can't convert to a numberint.Parse("abc")
IndexOutOfRangeExceptionYou read past the end of an arrayarr[10] in a size-3 array
DivideByZeroExceptionYou divide an int by 010 / 0
InvalidOperationExceptionAn object is in the wrong statelist.First() on empty list
ArgumentExceptionA method gets a bad argumentyou throw it on invalid input

Every one of these inherits from Exception, which is why a single catch (Exception ex) can catch them all — but, as you'll see, catching the specific type is usually better.

1. try / catch / finally

An exception is C#'s way of saying "I can't do this" at runtime — like parsing the text "abc" as a number. Left alone, it crashes your program. You put the risky code inside a try block; if it throws, C# jumps to a matching catch block instead of crashing. The optional finally block runs no matter what — perfect for cleanup. Read this worked example, run it, then you'll write your own.

using System;

class Program
{
    static void Main()
    {
        // A try block holds code that MIGHT fail (throw an exception).
        // If it fails, C# jumps straight to a matching catch block —
        // the rest of the try is skipped.

        try
        {
            Console.WriteLine("Before the risky line");
            int number = int.Parse("abc");   // 💥 "abc" isn't a number -> throws
            Console.WriteLine($"Got: {number}"); // SKIPPED — never runs
        }
        catch (Exception ex)
        {
            // ex is the exception object. ex.Message describes what went wrong.
            Console.WriteLine($"Caught a problem: {ex.Message}");
            // Caught a problem: The input string 'abc' was not in a correct format.
        }
        finally
        {
            // finally ALWAYS runs — crash or no crash. Great for cleanup.
            Console.WriteLine("Cleanup done (this always runs)");
        }

        // The program keeps going instead of crashing:
        Console.WriteLine("Program continues normally ✅");
    }
}

// ✅ Expected output:
//    Before the risky line
//    Caught a problem: The input string 'abc' was not in a correct format.
//    Cleanup done (this always runs)
//    Program continues normally ✅

Your turn. The program below would crash because "oops" isn't a number. Wrap it in a try/catch so it fails gracefully — fill in the blanks marked ___ using the hints.

using System;

class Program
{
    static void Main()
    {
        // 🎯 YOUR TURN — wrap the risky line so a bad value can't crash us.
        string userInput = "oops";   // pretend the user typed this

        // 1) Open a try block
        ___                          // 👉 the keyword  try
        {
            int age = int.Parse(userInput);   // 💥 this throws
            Console.WriteLine($"You are {age}");
        }
        // 2) Catch ANY exception into a variable called ex
        catch (___ ex)               // 👉 the general type  Exception
        {
            // 3) Print the message from the exception object
            Console.WriteLine($"Could not read your age: {___}");  // 👉 ex.Message
        }

        // ✅ Expected output:
        //    Could not read your age: The input string 'oops' was not in a correct format.
    }
}

2. Catching Specific Exceptions

Catching the broad Exception works, but it treats every problem the same. Usually you want to react differently to different errors — a bad index isn't the same as bad text. So you list several catch blocks, each for a specific type. The rule that trips everyone up: specific catches must come before the general one. C# checks them top to bottom and stops at the first match, so if catch (Exception) came first it would swallow everything and the compiler rejects the unreachable ones below it.

using System;

class Program
{
    static void Main()
    {
        try
        {
            int[] scores = { 90, 80 };
            Console.WriteLine(scores[5]);   // 💥 index 5 doesn't exist
        }
        // SPECIFIC catches come FIRST — most precise wins.
        catch (IndexOutOfRangeException ex)
        {
            Console.WriteLine($"Bad index: {ex.Message}");
            // Bad index: Index was outside the bounds of the array.
        }
        catch (FormatException ex)
        {
            Console.WriteLine($"Bad format: {ex.Message}");  // not hit here
        }
        // GENERAL catch comes LAST — the safety net for anything else.
        catch (Exception ex)
        {
            Console.WriteLine($"Something else: {ex.Message}"); // not hit here
        }
        finally
        {
            Console.WriteLine("Always tidy up here.");
        }
    }
}

// ✅ Expected output:
//    Bad index: Index was outside the bounds of the array.
//    Always tidy up here.

Now you try. Dividing an int by zero throws a specific exception. Catch exactly that type, then add a finally block that always runs.

using System;

class Program
{
    static void Main()
    {
        // 🎯 YOUR TURN — catch the EXACT error, then add a finally block.
        try
        {
            int a = 10;
            int b = 0;
            int result = a / b;     // 💥 dividing an int by zero throws
            Console.WriteLine(result);
        }
        // 1) Catch the SPECIFIC exception thrown by 10 / 0
        catch (___ ex)              // 👉 DivideByZeroException
        {
            Console.WriteLine($"Maths error: {ex.Message}");
        }
        // 2) Add a block that ALWAYS runs, even after an error
        ___                         // 👉 the keyword  finally
        {
            Console.WriteLine("Calculation finished.");
        }

        // ✅ Expected output:
        //    Maths error: Attempted to divide by zero.
        //    Calculation finished.
    }
}

3. Throwing & Re-throwing

You don't only catch exceptions — you can throw them too. When a method is handed input it can't work with, the cleanest response is throw new ArgumentException("...") so the caller knows immediately. If you catch an exception just to log it but can't fully handle it, re-throw with a bare throw;. Crucially, throw; keeps the original stack trace (the breadcrumb trail showing where the error really started); writing throw ex; instead resets it and hides the true source.

using System;

class Program
{
    // A method can THROW to signal "I can't do my job with this input".
    static int GetAge(string text)
    {
        if (string.IsNullOrWhiteSpace(text))
            throw new ArgumentException("Age cannot be blank.");

        return int.Parse(text);   // may throw FormatException too
    }

    static void Main()
    {
        try
        {
            int age = GetAge("");           // 💥 throws ArgumentException
            Console.WriteLine($"Age: {age}");
        }
        catch (ArgumentException ex)
        {
            // Log it, then re-throw with  throw;  to KEEP the original stack trace.
            Console.WriteLine($"Logged: {ex.Message}");
            // throw;     // ✅ re-throws same exception (stack trace preserved)
            // throw ex;  // ❌ resets the stack trace — avoid this
        }
    }
}

// ✅ Expected output:
//    Logged: Age cannot be blank.

🔎 Deep Dive: Custom Exceptions

When none of the built-in types describe your problem well, make your own. A custom exception is just a class that inherits from Exception. The big win is that it can carry extra data — here, exactly how much money is missing — so the catch block can give a genuinely helpful message instead of a generic one.

using System.Globalization;
using System;

// A custom exception is just a class that inherits from Exception.
// It can carry EXTRA data so the caller knows what went wrong.
class InsufficientFundsException : Exception
{
    public decimal Shortfall { get; }

    public InsufficientFundsException(decimal shortfall)
        : base($"You are {shortfall:C} short.")   // sets ex.Message
    {
        Shortfall = shortfall;
    }
}

class Program
{
    static void Withdraw(decimal balance, decimal amount)
    {
        if (amount > balance)
            throw new InsufficientFundsException(amount - balance);

        Console.WriteLine($"Withdrew {amount:C}.");
    }

    static void Main()
    {
        // Currency formatting follows the thread's culture: {value:C} prints
        // £ in London, $ in Boston and € in Paris. Pin it when the output has
        // to be the same everywhere — as it does on a page that shows you the
        // result.
        CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-GB");

        try
        {
            Withdraw(50m, 80m);   // 💥 not enough money
        }
        catch (InsufficientFundsException ex)
        {
            Console.WriteLine(ex.Message);            // You are £30.00 short.
            Console.WriteLine($"Top up {ex.Shortfall:C} to proceed.");
        }
    }
}

// ✅ Expected output:
//    You are £30.00 short.
//    Top up £30.00 to proceed.

Name custom exceptions ending in Exception by convention, and only create them when a built-in type genuinely doesn't fit.

4. When NOT to Use Exceptions

Exceptions are for the unexpected. A user typing letters into an age box is completely expected, so reaching for try/catch there is the wrong tool — it's slower and noisier. Use TryParse, which returns true/false instead of throwing. Separately, when you open something that must be closed (files, streams, connections), wrap it in a using block: it calls Dispose() for you automatically, even if an exception is thrown — the tidy version of a try/finally cleanup.

using System;

class Program
{
    static void Main()
    {
        // EXPECTED failures (bad user input) shouldn't use try-catch at all.
        // TryParse returns true/false instead of throwing — fast and clean.
        string input = "hello";

        if (int.TryParse(input, out int number))
            Console.WriteLine($"Parsed: {number}");
        else
            Console.WriteLine($"'{input}' is not a whole number"); // this runs

        // 'using' guarantees cleanup (Dispose) even if an error is thrown —
        // it's the tidy alternative to a try/finally that closes resources.
        using (var writer = new System.IO.StringWriter())
        {
            writer.Write("buffered text");
            Console.WriteLine($"Length: {writer.ToString().Length}"); // Length: 13
        } // writer.Dispose() is called automatically here
    }
}

// ✅ Expected output:
//    'hello' is not a whole number
//    Length: 13

Common Errors (and the fix)

Pro Tips

📋 Quick Reference

TaskCodeNotes
Catch any errorcatch (Exception ex)Put it last
Read the messageex.MessageHuman-readable text
Always runfinally { ... }Cleanup
Throw an errorthrow new ArgumentException("…")Signal bad input
Re-throwthrow;Keeps stack trace
Parse safelyint.TryParse(s, out int n)true / false
Auto-cleanupusing (var f = …) { … }Calls Dispose()

Frequently Asked Questions

Q: Should I just wrap my whole program in one big try/catch?

No. A giant catch hides where the error came from and tempts you to ignore it. Catch close to the risky operation, catch the specific type, and only handle what you can actually recover from.

Q: When do I use TryParse instead of try/catch?

Whenever failure is expected — typically user input or file content. Throwing and catching is slow; TryParse just returns false. Save exceptions for genuinely unexpected situations.

Q: What's the difference between throw; and throw ex;?

throw; re-throws the current exception and preserves the original stack trace (where it really started). throw ex; throws it again but resets the trace to this line, hiding the source. Almost always use throw;.

Q: Does finally run even if there's a return in the try?

Yes. finally runs whether the try finishes normally, throws, or returns early. That guarantee is exactly why it's the right place for cleanup.

Mini-Challenge: Safe Divider

No blanks this time — just a brief and an outline. Combine everything: parse user input safely, then divide inside a try/catch so neither bad text nor a zero can crash you. Run it with "4", "0", and "abc" to check all three paths.

using System;

class Program
{
    static void Main()
    {
        // 🎯 MINI-CHALLENGE: safe divider
        // 1. You're given some user text in 'input' (try "0", "4", "abc").
        // 2. SAFELY turn it into an int. If it isn't a number, print
        //    "Not a number" and stop.
        // 3. Otherwise divide 100 by it inside a try/catch and print the result.
        //    If they entered 0, catch DivideByZeroException and print
        //    "Cannot divide by zero".
        // Hint: int.TryParse for step 2; try/catch for the division.
        //
        // ✅ Expected outputs:
        //    input = "4"   ->  100 / 4 = 25
        //    input = "0"   ->  Cannot divide by zero
        //    input = "abc" ->  Not a number

        string input = "0";

        // your code here
    }
}

🎉 Lesson Complete

Practice quiz

What goes inside a try block?

  • Cleanup code that must always run
  • The catch handler
  • Code that might throw an exception
  • The exception's message

Answer: Code that might throw an exception. A try block holds the risky code that might throw; if it throws, control jumps to a matching catch.

When does a finally block run?

  • Always — whether or not an exception occurred
  • Only when an exception is thrown
  • Only when no exception is thrown
  • Only if you call it explicitly

Answer: Always — whether or not an exception occurred. finally always runs — crash or no crash, even after a return in the try — which makes it ideal for cleanup.

How must you order multiple catch blocks?

  • General before specific
  • Alphabetically
  • Order doesn't matter
  • Specific before general

Answer: Specific before general. Specific catches must come before the general catch (Exception); C# checks top to bottom and a general catch first would make the rest unreachable (CS0160).

What does ex.Message give you?

  • The line number
  • A human-readable description of what went wrong
  • The exception type name only
  • The full stack trace

Answer: A human-readable description of what went wrong. ex.Message is the human-readable description of the error carried on the exception object.

Which exception is thrown by `int.Parse("abc")`?

  • FormatException
  • IndexOutOfRangeException
  • DivideByZeroException
  • NullReferenceException

Answer: FormatException. Parsing text that isn't a valid number throws FormatException.

Which exception is thrown by dividing an int by zero (`10 / 0`)?

  • ArgumentException
  • FormatException
  • DivideByZeroException
  • InvalidOperationException

Answer: DivideByZeroException. Dividing an int by zero throws DivideByZeroException.

What is the difference between `throw;` and `throw ex;` in a catch block?

  • They are identical
  • throw; preserves the original stack trace; throw ex; resets it
  • throw ex; preserves the stack trace; throw; resets it
  • throw; rethrows a new exception type

Answer: throw; preserves the original stack trace; throw ex; resets it. A bare throw; re-throws the current exception and keeps the original stack trace; throw ex; resets the trace to that line, hiding the source.

How do you create a custom exception?

  • Implement IException
  • Add the [Exception] attribute
  • Override Console.Error
  • Write a class that inherits from Exception

Answer: Write a class that inherits from Exception. A custom exception is just a class that inherits from Exception, and it can carry extra data for the caller.

For expected failures like bad user input, what should you use instead of try/catch?

  • A bigger try block
  • int.TryParse (returns true/false)
  • throw new Exception
  • A finally block

Answer: int.TryParse (returns true/false). TryParse returns true/false instead of throwing — far faster and cleaner than try/catch for expected failures.

What does a `using` block guarantee for a disposable resource?

  • It catches all exceptions
  • It retries the operation
  • It calls Dispose() automatically, even if an exception is thrown
  • It logs the error

Answer: It calls Dispose() automatically, even if an exception is thrown. A using block calls Dispose() for you when the block ends, even on error — the tidy alternative to a try/finally that closes resources.

Continue this course