Skip to main content
    Courses/C#/Exception Handling

    Lesson 10 • Intermediate Track

    Exception Handling

    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.

    What You'll Learn

    • Wrap risky code in try/catch and read ex.Message
    • Use finally for cleanup that must always run
    • Catch specific exceptions (and order them correctly)
    • Throw your own errors with throw and re-throw with throw;
    • Write a simple custom exception that carries extra data
    • Use TryParse and using instead of exceptions where it fits

    💡 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.

    Worked example: try / catch / finally

    Read every comment, run it, and watch the program survive a crash.

    Try it Yourself »
    C#
    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
            }
            cat
    ...

    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.

    🎯 Your turn: catch a crash

    Fill in the ___ blanks to wrap the risky line and print ex.Message.

    Try it Yourself »
    C#
    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
            ca
    ...

    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.

    Worked example: specific before general

    See how the right catch block is chosen by exception type.

    Try it Yourself »
    C#
    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)
            {
    
    ...

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

    🎯 Your turn: specific catch + finally

    Catch the exact divide-by-zero error and add a finally block.

    Try it Yourself »
    C#
    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 er
    ...

    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.

    Worked example: throw and throw; (re-throw)

    See a method throw, and why throw; beats throw ex;.

    Try it Yourself »
    C#
    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}")
    ...

    🔎 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.

    Worked example: a custom exception

    An InsufficientFundsException that carries the shortfall amount.

    Try it Yourself »
    C#
    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 (amo
    ...

    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.

    Worked example: TryParse & using

    Handle expected failures without throwing, and clean up safely.

    Try it Yourself »
    C#
    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 a
    ...

    Common Errors (and the fix)

    • "CS0160: A previous catch clause already catches all exceptions": you put catch (Exception) before a more specific catch, making the lower one unreachable. Order specific → general.
    • Swallowing exceptions silently: an empty catch { } hides the bug — the program limps on in a broken state. At minimum log ex.Message; only catch what you can actually handle.
    • Catching Exception too broadly: a blanket catch also hides errors you never meant to handle (typos, null bugs). Catch the specific type, and let truly unexpected ones surface.
    • Re-throwing with throw ex;: this resets the stack trace so you lose where the error began. Use a bare throw; to preserve it.
    • Using exceptions for normal control flow: looping with try/catch to test if input parses is slow and unreadable. Use int.TryParse(...) — that's what it's for.

    Pro Tips

    • 💡 Catch specific, throw specific. The more precise the type, the more precise the fix — and the fewer real bugs you accidentally hide.
    • 💡 Re-throw with throw;, never throw ex; — keep the original stack trace intact.
    • 💡 Prefer TryParse over Parse for anything a user typed; it's far faster than throwing and catching.
    • 💡 Use using for anything disposable (files, streams) so cleanup happens even on error.
    • 💡 finally always runs — even if you return from inside the try. Put guaranteed cleanup there.

    📋 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.

    🎯 Mini-Challenge: safe divider

    Use TryParse for the text, try/catch for the division by zero.

    Try it Yourself »
    C#
    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
    ...

    🎉 Lesson Complete

    • try holds risky code; catch handles the error; finally always runs
    • ✅ Know the common types: NullReference, Format, IndexOutOfRange, DivideByZero, InvalidOperation
    • ✅ Order catches specific → general (general last) or you'll hit CS0160
    • throw to signal bad input; re-throw with throw; to keep the stack trace
    • ✅ Custom exceptions inherit from Exception and can carry extra context
    • ✅ Use TryParse for expected failures and using for guaranteed cleanup
    • Next lesson: LINQ — query, filter, and transform collections in one clean expression

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service