Exception Handling in C#

    Atul Kabra3 min readUpdated

    An exception is the .NET way of signalling that something went wrong while a program runs — a file is missing, input isn't a number, or a network call failed. If you ignore an exception, the program crashes. Exception handling lets you catch the problem, respond sensibly, and keep running. The core keywords are try (the risky code), catch (what to do when it fails), finally (cleanup that always runs), and throw (raise an exception yourself). Used well, they turn crashes into controlled, recoverable situations.

    The basic try / catch

    try
    {
        Console.Write("Enter a number: ");
        int n = int.Parse(Console.ReadLine()!); // throws if the text isn't a number
        Console.WriteLine($"Double is {n * 2}");
    }
    catch (FormatException)
    {
        // Runs only if int.Parse fails because the text wasn't a valid number.
        Console.WriteLine("That wasn't a valid number.");
    }
    

    If the user types "hello", int.Parse throws a FormatException, control jumps to the catch block, and the program continues instead of crashing.

    Catching specific exceptions

    Catch the most specific type first. Each exception type carries useful information:

    try
    {
        int[] scores = { 90, 80 };
        Console.WriteLine(scores[5]); // index 5 doesn't exist
    }
    catch (IndexOutOfRangeException ex)
    {
        // 'ex' holds details about what happened.
        Console.WriteLine($"Bad index: {ex.Message}");
    }
    catch (Exception ex)
    {
        // A general catch-all should come LAST, as a safety net.
        Console.WriteLine($"Something else failed: {ex.Message}");
    }
    

    Exception is the base type of all exceptions, so a catch (Exception) matches everything. Always place it last; otherwise the specific catches become unreachable.

    finally — cleanup that always runs

    The finally block runs whether or not an exception happened — ideal for releasing resources:

    try
    {
        Console.WriteLine("Opening connection...");
        throw new InvalidOperationException("Server unavailable");
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine($"Handled: {ex.Message}");
    }
    finally
    {
        // Runs no matter what — success or failure.
        Console.WriteLine("Closing connection.");
    }
    

    For things like files and streams, the using statement is even better — it disposes them automatically when the block ends, so you rarely need a manual finally for cleanup.

    Throwing your own exceptions

    Use throw to signal an error your code detects:

    public decimal Withdraw(decimal amount, decimal balance)
    {
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive");
        if (amount > balance)
            throw new InvalidOperationException("Insufficient funds");
    
        return balance - amount;
    }
    

    Pick a built-in type that fits the problem: ArgumentException for bad parameters, InvalidOperationException for an action that isn't valid right now.

    Want to learn this properly?

    Join the waitlist for our courses — beginner-friendly, project-first classes in Jalgaon.

    Browse courses

    Custom exceptions

    For domain-specific errors, define your own type by inheriting from Exception:

    public class CourseFullException : Exception
    {
        public CourseFullException(string message) : base(message) { }
    }
    
    // Later, in your enrollment logic:
    // throw new CourseFullException("Batch already has 30 students");
    

    A custom type lets callers catch (CourseFullException) and handle that one case precisely.

    Prefer prevention over catching

    Exceptions are for exceptional situations, not normal control flow. When you can validate cheaply, do so:

    // Better than try/catch for parsing user input:
    if (int.TryParse(Console.ReadLine(), out int value))
        Console.WriteLine($"Got {value}");
    else
        Console.WriteLine("Please enter digits only.");
    

    Common mistakes

    • Swallowing exceptions silently. An empty catch { } hides bugs. At minimum, log the error.
    • Catching Exception too broadly and too early. Catch specific types first; reserve the general catch for a genuine safety net.
    • Using exceptions for normal flow. Don't rely on a thrown exception to handle expected input — use TryParse and validation instead.
    • Forgetting cleanup. Use using for disposable resources, or finally to ensure they're released even on failure.
    • Throwing new Exception("..."). Choose a meaningful type so callers can handle it specifically.

    FAQ

    What's the difference between throw; and throw ex;? Plain throw; rethrows the current exception and preserves the original stack trace. throw ex; resets the stack trace, hiding where the error really started — avoid it.

    Should every method use try/catch? No. Handle exceptions where you can do something meaningful about them, often at a higher level.

    Keep learning

    Write robust, real-world C# with a mentor in Jalgaon — join the waitlist for the Infoplanet .NET course at /courses/dotnet.

    Want to learn this properly?

    Join the waitlist for our courses — beginner-friendly, project-first classes in Jalgaon.

    Browse courses
    Atul Kabra

    Founder, Infoplanet

    Atul Kabra founded Infoplanet in 2001 and has spent over two decades teaching programming — C, C++, Java, databases and more — to students across Maharashtra.

    Related guides