Day 9: Error Handling - Because Your Code Will Crash and That is Okay
arrow_back All Posts
April 06, 2026 8 min read .NET/C#

Day 9: Error Handling - Because Your Code Will Crash and That is Okay

Welcome back! On Day 8 you became a string wizard โ€” slicing, searching, and formatting text like a pro. Today we face an uncomfortable truth: your code will crash. Not "might." Will. The question isn't whether errors happen โ€” it's whether your program falls apart like a house of cards or handles them gracefully like a seasoned professional. ๐Ÿ›ก๏ธ

1. Why Errors Happen (And Why That's Normal)

Imagine you're a waiter at a restaurant. A customer orders a steak. Normally, the kitchen makes it, you deliver it, everyone's happy. But what if:

  • The kitchen is on fire (server error)
  • The customer ordered something not on the menu (bad input)
  • You dropped the plate on the way to the table (runtime error)

In all these cases, you don't just stand there frozen โ€” you handle the situation. You apologize, you find a solution, you keep the restaurant running. That's exactly what exception handling does for your code.

In C#, when something goes wrong at runtime, the system throws an exception โ€” an object that describes what went wrong. If nobody catches it, your program crashes. If you catch it, you decide what happens next.

2. Your First try/catch

The basic structure is simple:

try
{
    Console.Write("Enter a number: ");
    string input = Console.ReadLine() ?? "";
    int number = int.Parse(input);
    Console.WriteLine($"You entered: {number}");
}
catch (FormatException)
{
    Console.WriteLine("That wasn't a valid number!");
}
  • try โ€” wraps the code that might throw an exception.
  • catch โ€” runs only if an exception of the specified type is thrown.

If the user types "hello" instead of a number, int.Parse throws a FormatException. Without the try/catch, the program crashes with a scary stack trace. With it, the user sees a friendly message.

3. Catching Different Exception Types

You can catch multiple types of exceptions, each with their own response:

try
{
    Console.Write("Enter a number to divide 100 by: ");
    int divisor = int.Parse(Console.ReadLine() ?? "");
    int result = 100 / divisor;
    Console.WriteLine($"100 / {divisor} = {result}");
}
catch (FormatException)
{
    Console.WriteLine("That's not a number!");
}
catch (DivideByZeroException)
{
    Console.WriteLine("You can't divide by zero! Math doesn't work that way.");
}
catch (Exception ex)
{
    Console.WriteLine($"Something unexpected happened: {ex.Message}");
}

Order matters! C# checks catch blocks from top to bottom and uses the first match. Since Exception is the base class for all exceptions, it must go last โ€” otherwise it would catch everything and the specific handlers would never run.

Think of it like a mail sorting room: check for specific addresses first, then have a "catch-all" bin at the end.

4. The finally Block โ€” Always Runs, No Matter What

Sometimes you need code to run whether or not an exception occurred. That's what finally is for:

StreamReader? reader = null;
try
{
    reader = new StreamReader("data.txt");
    string content = reader.ReadToEnd();
    Console.WriteLine(content);
}
catch (FileNotFoundException)
{
    Console.WriteLine("File not found!");
}
finally
{
    reader?.Dispose();
    Console.WriteLine("Cleanup complete.");
}

finally runs in all cases:

  • No exception? finally runs.
  • Exception caught? finally runs.
  • Exception not caught? finally still runs (right before the program crashes).

It's the "I don't care what happened, just clean up after yourself" block. Perfect for closing files, database connections, or network streams.

5. The Exception Object โ€” What's Inside

When you catch an exception, you get access to an object full of useful information:

try
{
    int[] numbers = [1, 2, 3];
    Console.WriteLine(numbers[10]);
}
catch (IndexOutOfRangeException ex)
{
    Console.WriteLine($"Message: {ex.Message}");
    Console.WriteLine($"Type: {ex.GetType().Name}");
    Console.WriteLine($"Stack Trace: {ex.StackTrace}");
}

Key properties every exception has:

  • Message โ€” human-readable description of what went wrong
  • StackTrace โ€” the exact path the code took to reach the error (your debugging breadcrumbs)
  • InnerException โ€” if this exception was caused by another exception, it's nested here
  • Source โ€” the name of the assembly that threw the exception

In production code, always log the full exception (including stack trace), but show users a friendly message โ€” nobody wants to see System.NullReferenceException: Object reference not set to an instance of an object on their screen.

6. Common Exception Types You'll Meet

Here are the exceptions you'll encounter most often as a C# beginner:

  • NullReferenceException โ€” you tried to use an object that is null. The #1 most common exception in all of .NET. We'll tackle null properly on a later day.
  • FormatException โ€” a string couldn't be converted to the expected type (int.Parse("hello")).
  • IndexOutOfRangeException โ€” you accessed an array index that doesn't exist (arr[99] on a 3-element array).
  • DivideByZeroException โ€” you divided an integer by zero.
  • FileNotFoundException โ€” you tried to open a file that doesn't exist.
  • InvalidOperationException โ€” you called a method at the wrong time (like reading from an empty collection).
  • ArgumentNullException โ€” you passed null to a method that doesn't accept it.
  • ArgumentOutOfRangeException โ€” you passed a value outside the valid range.
  • OverflowException โ€” a number exceeded its type's maximum value (in a checked context).

7. TryParse โ€” The "Polite" Way to Handle Bad Input

Using try/catch for expected bad input (like user typing letters instead of numbers) works, but it's expensive. Throwing exceptions is slow. For parsing, use the TryParse pattern instead:

Console.Write("Enter a number: ");
string input = Console.ReadLine() ?? "";

if (int.TryParse(input, out int number))
{
    Console.WriteLine($"You entered: {number}");
}
else
{
    Console.WriteLine("That wasn't a valid number.");
}

TryParse returns bool โ€” true if it worked, false if it didn't. No exceptions, no overhead. The parsed value lands in the out variable.

Rule of thumb: use TryParse for user input validation. Use try/catch for genuinely exceptional situations you can't predict or prevent (file system errors, network failures, etc.).

Most built-in types have TryParse: int.TryParse, double.TryParse, DateTime.TryParse, Guid.TryParse, and more.

8. Throwing Your Own Exceptions

Sometimes you are the one who needs to signal that something went wrong:

static double CalculateBMI(double weightKg, double heightM)
{
    if (heightM <= 0)
        throw new ArgumentOutOfRangeException(nameof(heightM), "Height must be positive.");
    if (weightKg <= 0)
        throw new ArgumentOutOfRangeException(nameof(weightKg), "Weight must be positive.");

    return weightKg / (heightM * heightM);
}

try
{
    double bmi = CalculateBMI(80, 0);
}
catch (ArgumentOutOfRangeException ex)
{
    Console.WriteLine($"Invalid input: {ex.Message}");
}

Use throw to launch an exception up the call stack. The first matching catch block (yours or a caller's) handles it.

When to throw:

  • The caller gave you invalid arguments (ArgumentException, ArgumentNullException, ArgumentOutOfRangeException)
  • The object is in a state where the operation doesn't make sense (InvalidOperationException)
  • Something genuinely unexpected happened

When NOT to throw:

  • For normal control flow (use if/else instead)
  • For expected failures like "user typed bad input" (use TryParse)

9. The using Statement โ€” finally's Cooler Cousin

Remember that finally block for closing the StreamReader? There's a cleaner way:

// The using statement automatically calls Dispose() when the block exits
using (StreamReader reader = new StreamReader("data.txt"))
{
    string content = reader.ReadToEnd();
    Console.WriteLine(content);
}
// reader.Dispose() is called automatically here โ€” even if an exception occurred

// Even shorter โ€” C# 8+ using declaration (no braces needed)
using StreamReader reader2 = new StreamReader("data.txt");
string content2 = reader2.ReadToEnd();
// Disposed at end of enclosing scope

using works with any object that implements IDisposable โ€” files, database connections, HTTP clients, etc. It's the idiomatic C# way to ensure cleanup happens.

10. Your Homework: Safe Calculator

Build a calculator that:

  1. Asks for two numbers and an operator (+, -, *, /)
  2. Handles invalid number input gracefully (use TryParse)
  3. Handles division by zero
  4. Keeps asking until the user types "quit"
while (true)
{
    Console.Write("Enter first number (or 'quit'): ");
    string input1 = Console.ReadLine() ?? "";
    if (input1.Equals("quit", StringComparison.OrdinalIgnoreCase)) break;

    Console.Write("Enter operator (+, -, *, /): ");
    string op = Console.ReadLine() ?? "";

    Console.Write("Enter second number: ");
    string input2 = Console.ReadLine() ?? "";

    if (!double.TryParse(input1, out double num1) ||
        !double.TryParse(input2, out double num2))
    {
        Console.WriteLine("Invalid number. Try again.");
        continue;
    }

    try
    {
        double result = op switch
        {
            "+" => num1 + num2,
            "-" => num1 - num2,
            "*" => num1 * num2,
            "/" => num2 != 0 ? num1 / num2
                   : throw new DivideByZeroException(),
            _ => throw new InvalidOperationException($"Unknown operator: {op}")
        };
        Console.WriteLine($"{num1} {op} {num2} = {result}");
    }
    catch (DivideByZeroException)
    {
        Console.WriteLine("Cannot divide by zero!");
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine(ex.Message);
    }
}
Console.WriteLine("Goodbye!");

Notice how TryParse handles the "expected" bad input (non-numeric text), while try/catch handles the "exceptional" cases (division by zero, unknown operator). That's the right pattern.

Bonus challenge: add a try/catch around the entire while loop to catch any truly unexpected exception, log it with ex.ToString(), and keep the calculator running.

For the full exception handling reference, see the C# exception handling docs on Microsoft Learn.

Summary of Day 9

  • try/catch wraps risky code and handles exceptions gracefully instead of crashing.
  • Catch specific exceptions first, then general Exception last โ€” order matters.
  • finally always runs โ€” use it for cleanup (or better yet, use using).
  • The exception object gives you Message, StackTrace, and InnerException for debugging.
  • TryParse is the right tool for validating user input โ€” no exceptions, no overhead.
  • throw your own exceptions when callers break your method's contract.
  • using is the idiomatic way to ensure Dispose() is called โ€” cleaner than finally.
  • Golden rule: exceptions are for exceptional situations, not normal control flow.

Tomorrow: we'll talk about Classes and Objects โ€” the big one. Everything in C# is an object, and it's time you learned to build your own. Your programs are about to go from "scripts" to "real software." ๐Ÿ—๏ธ

See you on Day 10!

Share
FM

Farhad Mammadov

.NET Engineer & Cloud Architect ยท Bayern, Germany. Writing about scalable backend systems, AWS, and SRE.