7 min read

Exceptions

Crash Course

Introduction

Take a look at the code below, and tell me if you can find where we might have a problem:

Console.Write("Enter a number: ");
string input = Console.ReadLine();
int number = Convert.ToInt32(input);

If the user enters a number, like "123", we’ll be fine. But what if they enter "abc"?

Many times, throughout this tutorial set, I’ve mentioned that, “If X happens, then your program will crash.” Crashing is, of course, a very bad thing. In this tutorial, we’re going to address that.

Exceptions

When something out of the ordinary happens, and code can’t correctly respond to some bad state or situation, the code cannot reasonably continue onward. The hope is that something else in the software understands how to recover from the problem.

What happens in C# code is that when these error states occur, a special object that represents the problem called an exception is created. The natural flow of execution abruptly stops, and we begin searching for something that can handle the exception. The start of this process is called throwing the exception.

Whatever line of code the exception began at, the system will look first in the method that contains that line for a handler. If it cannot find it there, it will go up to the method that called it to see if there’s a handler there. If that method doesn’t have a handler, then we continue up to the method that called it, and then method that called that. Up and up and up, until we find a handler, or until we reach the top-most method: the main method. If the main method cannot handle the error, then the program will end by crashing.

Which means if we can provide a handler that can reasonably address the problem in a method in the middle, we can prevent a crash.

try/catch Blocks

Handling an exception is done with a try/catch block. These are far easier to show than describe, so here is code that will ensure that even if the user enters bad text in our previous example, the program doesn’t crash:

try
{
    Console.Write("Enter a number: ");
    string input = Console.ReadLine();
    int number = Convert.ToInt32(input);
    Console.WriteLine(number + " is a valid number.");
}
catch (Exception e)
{
    Console.WriteLine("That was not a valid number.");
}

Before we can catch any exceptions, we must start by indicating that we’re about to run code that might result in an exception. This is what the try and the curly braces are for.

The catch (Exception e) part, along with the curly braces, defines a handler in case an exception is thrown. This says, “If you encounter an exception of any type, here is the code to run to recover.”

The Exception e part actually defines a variable that you can use inside of the catch block, to inspect the problem in more detail. If you don’t use the variable, you could also write it as:

catch (Exception)
{
    // ...
}

Handling Specific Exception Types

catch (Exception) will handle any type of error, regardless of what the error was. Most of the time, that is far too broad to be desirable. Different types of exceptions need to be handled in different ways. And sometimes, you can correctly recover from one type of error, but not another, and you have to rely on another handler defined elsewhere to save you.

Different categories of errors are represented by different classes, all ultimately derived from Exception. There are two ways Convert.ToInt32 could fail: the string might not contain a number, such as our "abc" example, or maybe it is legitimately a number, but so big that it can’t be correctly represented by the int type, which tops out at about 2 billion. It would be nice to handle those separately. We can do that with two separate catch blocks that are each focused on their specific type:

try
{
    Console.Write("Enter a number: ");
    string input = Console.ReadLine();
    int number = Convert.ToInt32(input);
    Console.WriteLine(number + " is a valid number.");
}
catch (FormatException)
{
    Console.WriteLine("Your input is not a number.");
}
catch (OverflowException)
{
    Console.WriteLine("That number is too big! Try a smaller number next time.");
    // We would also get in here if the number is too big in the negative direction,
    // such as -20 billion. The text above is slightly misleading then.
}

It is important to point out that catch blocks will be evaluated in the order presented, and only one will run. Consider this version:

try
{
    Console.Write("Enter a number: ");
    string input = Console.ReadLine();
    int number = Convert.ToInt32(input);
    Console.WriteLine(number + " is a valid number.");
}
catch (FormatException)
{
    Console.WriteLine("Your input is not a number.");
}
catch (Exception)
{
    Console.WriteLine("Something went wrong.");
}

FormatException is derived from Exception. Without the specific catch (FormatException) handler, it would still get addressed with the catch (Exception) handler. But because it comes first, anything that is a FormatException will be handled by the first catch block and not the second.

If you reversed the order of the two blocks, such that catch (Exception) were first, then the catch (FormatException) would never get used.

Throwing Exceptions

You will probably catch more exceptions than you throw, but if you detect a problem that you can’t recover from, you can throw your own exceptions with the throw keyword:

void Count(int amount)
{
    if (amount < 0 || amount > 1000)
        throw new ArgumentOutOfRangeException();

    for (int current = 1; current <= amount; current++)
        Console.WriteLine(current);
}

tip

There are a lot of exception types that come with C# automatically, including Exception, FormatException, OverflowException, and ArgumentOutOfRangeException. (You are likely to also encounter NullReferenceException.) If one of these existing exception types is a good fit for what you’re doing, you should use it. If there isn’t a good exception type that covers your problem, you should make a new exception type by [deriving]({{ref inheritance}}) a new type from Exception. Don’t get lazy and just use throw new Exception() everywhere.

finally Blocks

One complicating factor that happens with exceptions is that the flow of execution can jump away while you’re in the middle of doing things. Consider this code:

int shouldAlwaysBeEven = 0;

shouldAlwaysBeEven++; // Not even for a short period of time.
PossiblyThrowAnException();
shouldAlwaysBeEven++; // As long as this line runs, we'll keep the number even.

The problem with this code is that we have to make several changes to our data, and if an exception is thrown and we jump away to a catch block elsewhere, we may inadvertently leave our data in an inconsistent state.

note

The example above is a toy example to illustrate the problem. We’d normally just do shouldAlwaysBeEven += 2; and not call a method that just arbitrarily throws exceptions.

A finally block is a section of code, attached to a try block that will run any time you leave the try block regardless of if that was done by reaching the end naturally, hitting a return statement, or because an exception was thrown. A finally block lets you make sure you leave things in a happy and consistent state.

try
{
    shouldAlwaysBeEven++; // Not even for a short period of time.
    PossiblyThrowAnException();
    shouldAlwaysBeEven++; // As long as this line runs, we'll keep the number even.
}
finally
{
    if (shouldAlwaysBeEven % 2 != 0) // The remainder when dividing by 2 will be zero if it is an even number
        shouldAlwaysBeEven = 0; // Perhaps not the greatest response, but it ensures the data stays even, which
                                // is currently all that is expected of this variable.
}

It is quite common to include catch blocks with this:

try
{
    shouldAlwaysBeEven++; // Not even for a short period of time.
    PossiblyThrowAnException();
    shouldAlwaysBeEven++; // As long as this line runs, we'll keep the number even.
}
catch (Exception)
{
    Console.WriteLine("Something went wrong.");
}
finally
{
    if (shouldAlwaysBeEven % 2 != 0) // The remainder when dividing by 2 will be zero if it is an even number
        shouldAlwaysBeEven = 0; // Perhaps not the greatest response, but it ensures the data stays even, which
                                // is currently all that is expected of this variable.
}