Error Handling: Exceptions

Error Handling: Exceptions

The Crash Course

  • When things go wrong, exceptions provide a way to have the part of the code that discovered the problem indicate that something has gone wrong and return execution back to (hopefully) some other part of the code that knows how to deal with the problem that was reported.
  • If you know you are running code that could potentially cause a problem, you wrap it in a try/catch block, putting the type of exception in the parentheses of the catch block: try { /* code that may throw an exception here */ } catch(Exception e) { /* code to handle the problem here */}
  • You don't just need to use the Exception class; you can use a type derived from the Exception class, such as the FormatException class. Only exceptions of the right type will be handled when you do this, and others will continue to be "thrown" up to higher levels of the call stack.
  • You can string together multiple catch blocks, going from most specific to least specific types of exceptions: try { } catch(FormatException e) { /* handle format errors here */ } catch(Exception e) { /* handle all other errors here */ }
  • You can create your own types of exceptions by deriving from the Exception class.
  • To actually start off the process in your own code, you use the throw keyword: throw new Exception("An error has occurred.");

Introduction

Sometimes things go wrong. It happens. Maybe the user typed in a letter when they should have typed in a number. Maybe the user clicked on the wrong buttons in exactly the wrong order. Maybe a butterfly flapped its wings, causing a hurricane (or typhoon, if you live in those parts of the world), which caused an earthquake, which caused a tree to fall directly onto your network cable, destroying it (all, somehow, without you knowing it!).

The point is, that anything can go wrong, and in this tutorial, we'll talk about how to handle it gracefully—meaning that it is done in a way that minimizes the effect on the user and allows the section of code that caused the problem to resolve it.

Let's take the example of the user typing in bad input, like a letter or text, when they were supposed to provide a number. So perhaps our starting code is something like this:

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

If you don't already know what happens when you type in "abc" instead of a number, go ahead and give it a try.

If you are running in debug mode, the program will freeze, and Visual C# Express will pop up at the location where there was a problem—on the Convert.ToInt32 line. If you run without debugging, your program will simply die.

We've run into a problem and need a way to deal intelligently with it.

Fortunately for us, C# has a built-in way of taking care of things like this: exceptions.

Exception handling is a powerful tool for handling errors. Let's say that you have a method that is trying to do something (like lower Batman into a shark-filled tank), and it discovers that there's something horribly wrong (like somehow, Batman has escaped… again…). Everything is in a terrible state at this point and the method that discovered it doesn't necessarily know what caused the problem or how to fix it. It just knows it can't proceed.

Exceptions provide a way for any method to say, "I've run into a problem, and I can't continue in any intelligent way." This error, along with possibly some information about the problem, is sent back to the place that the method or code was called from, hoping that someone else knows what to do with the problem.

If something knows what to do, it intercepts the message and does something useful. Perhaps that means telling the user that there's a problem and trying again.

If nothing is able to handle the problem, it keeps going up to higher and higher method calls, all the way until it gets back to the Main method. If even that doesn't handle the error, the program terminates. (And the user is left wondering how they'll recover their lost file.)

The idea, though, is that when a problem comes up, something will be able to handle it, and the program will be able to continue in some sort of fashion, despite the problem, because the exception was handled.

Catching Exceptions

Exceptions are kind of like the game of Hot Potato. If you've never played the game before, the concept is simple. A small group of people stands around in a circle, throwing an object (called the "potato", whether it is an actual potato or not) around from person to person as quickly as possible. In the game, there's music playing, and when it ends, whoever has the potato in their hands loses. So you try to throw the potato away to the next person as quickly as possible.

When there's a problem in your program, and something bad happens, and the computer just doesn't have a clue how to fix it, an error message, packed neatly into an object of the type Exception is created and thrown very quickly up the chain to the method that called it. The method that the problem gets called in doesn't finish running, nor does it return a value. It just stops dead in its tracks, creates the exception, and goes back to the place that called it.

Once it gets there, if that method doesn't handle the problem, it takes the exception and, just as quickly, throws it onto the method that called it. This keeps happening, up until someone finally says, "I know what to do with this problem," or until it reaches your main method and gets thrown again, which causes the program to simply die. (Or, again, if you are running in debug mode, it will open up Visual C# Express right there, where the problem ("error") first occurred.

So we want to figure out who is responsible for handling the problem and find a way to intercept the exception and do something intelligent with it.

We can do this with what is called a try/catch block. This introduces two new keywords that we haven't seen yet: try and catch. Basically, we're going to say, "I've got this code that I know may result in a problem. Try to run it, and if there's an error or exception, catch the exception, and handle it using this other block of code."

This is done like this:

Console.Write("Enter a number:  ");
string userInput = Console.ReadLine();
 
try
{
    // The code that may have problems goes in here...
    int number = Convert.ToInt32(userInput);    
 
    // It is important to point out that if there was an error in 
    // the line above, the program would jump down to the
    // catch block below and any code right here would not
    // be executed.
}
catch(Exception e)
{
    // The code to handle the problem goes here.
    // Notice that if nothing goes wrong, this code
    // never gets executed.
    Console.WriteLine("You must enter a number.");
}

So, in short, you take your "dangerous" code, place it inside of a try block, and then immediately after the try block, you put a catch block that has code to handle the problem. If something goes wrong in the middle of the "dangerous" code, the rest of that dangerous code won't get executed. It jumps down to the error handling code in the catch block. On the other hand, if everything goes as planned, the stuff in the catch block will never be executed.

Take another look, though, at the stuff in parentheses at the start of the catch block: Exception e. There are two things that we should bring up here. One, in this line, we're creating a variable named e (you could call it exception, or cheeseburger, or whatever you want) that you can then use inside of the catch block. For instance, the Exception class has a property called Message, so you could say Console.WriteLine(e.Message); and print out that message. The Exception object that you get here may be storing information about what went wrong, and you can look at it to figure out what happened.

Second, when an exception is thrown, it may be using the Exception class, or potentially, a derived class that is more specific to the type of problem that occurred. Going back to our bad user input example, this isn't actually throwing an Exception object, it is throwing a FormatException object, which is derived from the Exception class. (Or, if you want to get pedantic, it is throwing an Exception, but the specific type is the derived class, FormatException.)

In the code we looked at, the Convert.ToInt32 method can throw a FormatException, or an OverflowException, which happens when you give it text that is, in fact, a number (or we'd get the FormatException) but is way too big to put into an int. Something like "1038493982582058259250" would do it.

In the catch block, we could say catch (Exception e), and catch anything of that type (or derived from that type)or we could say catch (FormatException formatException), and only catch things that are of the type FormatException. Any OverflowException thrown will be sent up the call stack to see if something else can handle it.

So we can be selective about what exceptions to handle.

Not Giving the Exception Variable a Name

I mentioned earlier that with a catch block, you can use the exception you're catching (for example, Exception e) as a variable inside of the catch block. If you're not going to use that variable, you can just leave it as a type without a variable name:

try
{
    //...
}
catch (Exception)
{
    //...
}

Handling Different Exceptions in Different Ways

As we just discussed, different blocks of code can throw different exceptions. You can catch any of the different types, or you can catch them all (Pokemon]] theme music fills the air) with catch(Exception e), or you can choose which derived type you want to catch.

But what if you want to catch them all but handle each different kind of error differently?

You can simply add multiple catch blocks to a single try:

try
{
    int number = Convert.ToInt32(userInput);
}
catch (FormatException)
{
    Console.WriteLine("You must enter a number.");
}
catch (OverflowException)
{
    Console.WriteLine("Enter a smaller number.");
}
catch (Exception)
{
    Console.WriteLine("An unknown error occurred.");
}

Only one of these blocks will execute, though, which means you want to put more specific exception types at the top. If your first handler was catch (Exception), then it will handle all exceptions without… um… exception. The other handlers won't ever run.

Throwing Exceptions

So far, we've just talked about handling exceptions. But what if you are writing some of your own code, and you are aware of a potential problem that could occur that could stop you dead in your tracks? You can create and throw your own exceptions!

To throw an exception, simply use the throw keyword:

public void CauseProblemsForTheWorld()  //always up to no good...
{
    throw new Exception("Just doing my job!");
}

So it kind of feels like the return statement, but it throws exceptions instead of returning a value. You create the Exception object, just like any other object, with the new keyword, and away it goes.

Of course, you're not required to just simply use the Exception class. There are plenty of other options already defined. For instance, here's a small collection of some common exceptions and what they're for:

Exception Type What it Does
NotImplementedException If a method has been defined but not implemented yet, you can use this exception. If Visual C# Express automatically generates methods for you, it will put this in the body.
IndexOutOfRangeException You tried to access an array or something at an index that is beyond how big the array is.
InvalidCastException You tried to cast something to another type, but the type you tried to cast to wasn't the right kind of object.
FormatException The text you had is not in the right format for converting to something else (like letters in a string that is supposed to be turned into a number).
NotSupportedException You tried to do an operation that wasn't supported. For instance, make a method call at a time that didn't allow it.
NullReferenceException A variable for any kind of object actually contained null instead of an actual object, but whatever you were doing required something besides null.
StackOverflowException You see this all the time when you run out of space on the stack from calling too many methods. This is usually a result of a recursion that went bad.
DivideByZeroException You tried to divide by zero and got caught.
ArgumentException One of the parameters or arguments you sent to a method didn't match what was required by the method.
ArgumentNullException One of the arguments or parameters that you gave to a method was null, but the method requires something besides null.
ArgumentOutOfRangeException One of the arguments contained a value that the method couldn't intelligently do something with. For instance, if a method required a number between 1 and 10, but you gave it -13, you might see this kind of exception.

And, of course, if you don't see the type of exception you want, you can always create your own. All you need to do is make a class derived from Exception or another exception type.

This will look something like this:

// For all of you on the other side of the pond, if it makes 
// you feel any better, you can still call them "beef burgers" if you want!
public class AteTooManyHamburgersException : Exception 
{
    public int HamburgersEaten { get; }
 
    public AteTooManyHamburgersException(int hamburgersEaten)
    {
        HamburgersEaten = hamburgersEaten;
    }
}

With this class, you can now say:

throw new AteTooManyHamburgersException(125);

And:

try
{
    EatSomeHamburgers(32);
}
catch (AteTooManyHamburgersException hamburgerException)
{
    Console.WriteLine(hamburgerException.HamburgersEaten + " is too many hamburgers.");
}

Wow. I never thought I'd see the day when I had the word "hamburgerException" in my code. Perhaps we're nearing the End of Times after all.

The finally Keyword

There's one last thing we want to take a look at regarding exceptions: The finally keyword. At the end of a try/catch block, you can have a finally block (turning it into a try/catch/finally block). The code inside of the finally section will run no matter what. If we reach the end of the try without problems, the finally will run. If there is an exception, the finally will run. If we hit a return statement and leave the method, the finally will run.

Here's an example of how this works:

try
{
    // Do some stuff that might throw an exception here
}
catch (Exception)
{
    // Handle the exception here
}
finally
{
    // This code will always get executed, regardless of what happens in the try/catch block.
}

A finally block is a convenient place to put cleanup that must happen, because there is no escaping it.

However, because it could be running in a lot of different conditions, including in the middle of processing an exception, you may find a few of the more advanced types of statements are forbidden in a finally block. I won't get into too many details, because they aren't that common, and the compiler will tell you if you do. But an example is that a finally cannot contain a return statement because we might actually already be in the process of returning a value when we run the finally.

What's Next?

At first glance, error handling with exceptions may not seem intuitive or easy to use. There's all of this try/catch stuff floating around, and things often seem to feel more complicated than it needs to be. But don't worry; it's the kind of thing that you can learn a little at a time.

Try using try/catch blocks when you know there could potentially be a problem or if you see your program crash when it throws an exception.

Error handling like this allows you to handle any problems that might come up, and maybe most importantly, it allows the methods that discover the problem to be able to hand the problem off to the place that caused the problem, or at least to the place that can fix it.

Up next, we'll take a look at a way to treat methods as though they are objects, allowing us to pass them around as needed: delegates. Delegates will be an important part of where we'll go after that: events, which allow one part of our code to alert another part when certain things occur.