Lesson 9 Β· ~12 min Β· ← Lesson 8

Exceptions: when runtime goes wrong

Compile errors stop you before launch; exceptions are the problems that surface while running β€” bad input, missing keys, division by zero. C#'s answer: try the risky part, catch the failure, finally the cleanup.

try / catch: contain the blast

string[] inputs = { "42", "abc" };

foreach (string s in inputs)
{
    try
    {
        Console.WriteLine("parsed: " + int.Parse(s));
    }
    catch (FormatException)              // catch by TYPE β€” only this failure
    {
        Console.WriteLine("not a number: " + s);
    }
}

Without the try/catch, the second int.Parse would kill the whole program. With it, the failure is contained to one iteration and the loop continues. Catch the most specific type you can handle β€” catch (Exception) catches everything and usually hides bugs.

The exception object: what went wrong, in writing

int Divide(int a, int b)
{
    if (b == 0)
    {
        throw new Exception("division by zero is undefined");   // raise it yourself
    }
    return a / b;
}

try
{
    Console.WriteLine(Divide(10, 2));
    Console.WriteLine(Divide(1, 0));
}
catch (Exception e)                    // e IS the exception β€” read its Message
{
    Console.WriteLine("caught: " + e.Message);
}
finally
{
    Console.WriteLine("always runs");  // success OR failure β€” cleanup lives here
}

finally runs no matter what: success, caught failure, or an exception nothing caught. It's where you close files and connections β€” the engine here doesn't have files, but the shape is universal.

Types form a family β€” catch the branch you mean

Exception                      // the root β€” everything is one of these
└── SystemException
    β”œβ”€β”€ FormatException             // int.Parse("abc")
    β”œβ”€β”€ NullReferenceException      // used a null object
    β”œβ”€β”€ IndexOutOfRangeException    // array index past the end
    β”œβ”€β”€ DivideByZeroException       // int division by zero
    └── ArgumentException           // you passed something invalid
        └── ArgumentNullException   // ...specifically: null

Catching a parent catches all its children: catch (Exception e) catches everything; catch (SystemException e) catches the system family. Order multiple catch blocks most-specific-first β€” the first match wins.

Don't try/catch what you can avoid

The idiomatic split: exceptions for exceptional things, TryParse for expected ones:

string[] inputs = { "42", "xyz" };

foreach (string s in inputs)
{
    if (int.TryParse(s, out int n))     // failure is a normal outcome here
    {
        Console.WriteLine("got " + n);
    }
    else
    {
        Console.WriteLine("ignored " + s);
    }
}

Rule of thumb: if "it failed" is a normal branch in your logic (user input!), use the Try-method. If failure means something is genuinely broken, let the exception fly and catch it at the level that can actually do something about it.

Throwing your own

When YOUR method gets an argument it can't work with, the C# convention is to throw ArgumentException (or one of its more specific children) with a message saying what's wrong:

void SetAge(int age)
{
    if (age < 0)
    {
        throw new ArgumentException("age cannot be negative: " + age);
    }
    // ...store it
}

Throw early, with a clear message β€” the alternative (silently storing nonsense) becomes someone else's 2 a.m. debugging session.

Practice this now