Coding With Flavio
Lesson 14: Exception Handling
Finally learn the proper way to handle incorrect input from the user. Make your code nice and robust.
Lesson Summary:
Exceptions
An exception is a situation in your code where something went wrong. It could have to do with something failing in your code logic, or maybe a network connection failing, or it could even have to do with some sort of user error. When an exception happens, we refer to the exception being "thrown". That means that the exception is announced for something to handle it, which we refer to as "catching" or "handling" that exception. We've seen exceptions being thrown in several scenarios, such as using int.Parse on a string that is not an integer, or indexing a list or array for a non-existent index such as -1.
An unhandled exception is a situation where there is a thrown exception and nothing catches the exception. This usually results in your program crashing. For example, in previous lessons, when we had an error due to int.Parse trying to parse a non-number string, an exception was thrown but it was not handled, thus crashing our program. This lesson will teach two things:
-
How to handle thrown exceptions
-
How to avoid throwing exceptions in situations where it can be avoided
The overall art of dealing with exceptions, both proactively and retroactively, is usually referred to as exception handling.
try / catch
The most common form of exception handling in C# is using a try/catch blocks. The idea is that you try to do a thing, and if something happens in the process and an exception is thrown, your catch block will catch and handle that exception. Take this example:
static void Main(string[] args)
{
try
{
int.Parse("abc");
}
catch
{
Console.WriteLine("Uhoh! An error occurred!");
}
Console.WriteLine("End of program!");
}
As we may know by now, int.Parse("abc") will throw an exception. Normally, this would crash our program. However, since int.Parse is within our try block, that exception will be caught by the catch block, and will lead to us writing "Uhoh! An error occurred!" to the console. After that, the program resumes normally, right after the catch's closing brace, }. As a result, we will print "End of program!" to the console. Note that without the try/catch, this code would not reach the line that prints "End of program!", as the program would crash before that.
In the example above, if we have "int.Parse("123");" instead of "int.Parse("abc");", there will not be an exception thrown. As a result, we will not reach the inside of the catch block. In other words, we only run the code inside the catch if there is an unhandled exception. Very nifty!
Nested try/catch
It's very common for try/catch statements to be nested, either within the same method, or thru methods within try blocks that have their own internal try/catches. Larger programs are typically structured in such a way. Here is an example of nested try/catch statements:
static void Main(string[] args)
{
try
{
try
{
int.Parse("abc");
}
catch
{
// This inner catch will catch the int.Parse
}
}
catch
{
// This outer catch will *not* catch the int.Parse
}
}
Note that the inner-most catch will handle the exception. The outer catch will not handle it in this case unless it is rethrown or a new exception is created. Note the following case as well:
static void DoStuff()
{
int.Parse("abc");
}
static void Main(string[] args)
{
try
{
DoStuff();
}
catch
{
// Will handle the int.Parse exception
}
}
In this scenario, the int.Parse("abc") exception thrown by DoStuff() will be caught by the try/catch block in Main since DoStuff() is called within the try block. Thus, even though there is no try/catch defined in DoStuff(), we still handle this exception because there is a try/catch wrapping the call at some point before the call. As long as we are in a try block somewhere, and that try has a viable catch, that exception will be handled.
Proactive error handling
Most of what we've been doing with try/catch has been reactive error handling. That is, handling an error after it happens. A better approach to error handling is often to handle the error before it happens. For example, one common error we see is a "null reference exception". For example, say we have have a List<int> that we forget to instantiate using new:
List<int> myBadList = null;
If you check the value of myBadList with the debugger, you'll see that there is no instance of list there; instead, it says the value is "null", meaning this variable has nothing associated with (I'll talk more about null and the concept of a "reference" in a future episode). If you do the following, you will get a NullReferenceException:
List<int> myBadList = null;
myBadList.Add(123);
This is because we are trying to call a member of a non-existing object. We could use try/catch to catch this exception, but if we expect that myBadList could ever be null, it would be better to do something like this:
List<int> myBadList = null;
if (myBadList != null)
{
myBadList.Add(123);
}
This example is obviously contrived because we know the list is always null here. However, there are scenarios where this check could make quite a bit of sense. For example:
void AddValuesToList(List<int> theList)
{
if (theList== null)
{
// Report your error by console, logging, or a message box popup
Console.WriteLine("Uhoh! Passed in a null list! Check the code!!");
}
else
{
theList.Add(123);
theList.Add(456);
theList.Add(789);
}
}
Here we have a method that takes a list and adds values to it. Say we made this method accessible to other programmers (such as yourself in the future); we don't always have control here of whether or not they (or you!) might pass in a null list. If a null list is passed in, this will throw a null reference exception. However, we can prevent that, and optionally report it as a coding problem to whomever used the method AddValuesToList so that they ensure they passed in a null list intentionally.
Code which is particularly resillient to special situations (such as null values or special, unexpected values) is referred to as being Robust. Robust code will not crash easilly if used in unexpected ways. You should make your own code robust even as a one-man team, since it will save you future headaches caused by your own design choices.
TryParse
We've used int.Parse as a constant example for exceptions, despite being a method that throws exceptions easily. Mostly this is because it's a simple method to use and understand. If you explore the API, you may notice a slightly more complicated version of this one called int.TryParse. In some situations, methods that throw exceptions may have a separate version of the method called Try* where * is the name of the method that throws the exception. Usually these allow you to attempt an action that normally calls an exception by returning true or false based on success, rather than throwing exceptions upon failure. For example, see the declaration for int.TryParse:
static bool TryParse(string s, out int result)
The first thing you may notice is that it returns bool. So, you may wonder, how does one get the result from the parsed string? Well, you might also note the result parameter, as well as an oddball keyword out right before it. This is an example of a method that features "out parameters". Methods such as this return additional values by passing them back via their parameters. For example, to use TryParse, we must create a variable that will hold the value of the result and pass that as a parameter:
int result;
TryParse("123", out result);
Notice that we declare "int result" before calling TryParse and we must also specify "out" when we pass it into the function; this basically specifies that we expect TryParse to set some sort of value for this argument after we call it. Note that in this example, we did not make use the bool return value. We can use it as follows:
string input = Console.ReadLine();
int result;
if (TryParse(input, out result)
{
// Parsed successfully!
}
else
{
// Parse failed; report an error to user and try a different input
}
Note that by doing it this way, we avoid having to catch and throw errors. Also, you may notice that this shouldn't require too much additional work to expand into a program that will re-prompt the user for input upon getting a bad value. The Practice problem will in fact focus on that.
Why exactly proactive error handling is desirable is not totally clear at this point, and some may argue that it's a preferential thing, but as you develop more code you may start finding pros and cons to proactive versus reactive exception handling based on your own coding preferences.
Additional Notes and Tips:
Exception Handling In Practice
If you think that a method has the chance to throw an exception, it's good to have a try/catch block at least somewhere in your code which will eventually handle that exception, or your program may crash. Note that this is not always due to misusing code, as many thrown exceptions can be legitimate situations. For example, if you use a method to connect to the internet to download some data from a site, it's feasible that this method could throw an exception if your computer's internet connection is not working.
Unity itself is quite coservative about exception handling, effectively catching any exceptions that may be caused by your own code to prevent it from ever crashing, and reporting them to the console so that you know something isn't quite right. Nonetheless, just because your program didn't crash does NOT mean you should ignore exceptions! This is VERY important and is a common rookie mistake!! If you ever see an unhandled exception being logged or reported, particularly in Unity, you MUST look into it and ensure that it's not intentional. The (very realistic) scenario that you should aim for is to not have any exceptions appearing in the console. We'll discuss this more in the actual Unity lessons. Having a bunch of random errors is a sign of poorly written code.
Custom Exceptions
(Note: this topic is a bit more advanced)
Sometimes you want to throw your own exceptions to be used in try/catch blocks. This could be because you want to create a class that properly reports issues, or maybe because you want to catch an exception and "rethrow" it with a different error. For example, take the previous example:
void AddValuesToList(List<int> theList)
{
if (theList== null)
{
// Report your error by console, logging, or a message box popup
Console.WriteLine("Uhoh! Passed in a null list! Check the code!!");
}
else
{
theList.Add(123);
theList.Add(456);
theList.Add(789);
}
}
The Console.WriteLine isn't particularly helpful if you want the caller of the code to be able to easily notice or handle this error, particularly because they might not be using a console application in the first place. We can do this instead:
void AddValuesToList(List<int> theList)
{
if (theList== null)
{
throw new Exception("To use AddValuesToList, be sure to pass in a non-null list");
}
else
{
theList.Add(123);
theList.Add(456);
theList.Add(789);
}
}
If other people (or you in the future) use this code, this exception could help you find a bug by reporting that the parameter was null when it should not have been. Note that we use "new Exception" to create an instance of our own Exception object with a message. We are using one of the constructors for the Exception class to set the Message that will be visible when viewing the error. There are other constructors that you can use, which I recommend you observe using Visual Studio's Intellisense. Also, note the keyword "throw"; this keyword can only be used with objects of type Exception and it will throw an exception the same way int.Parse does.
In the future (after covering inheritance) I will discuss how to make your own custom Exception classes similar to FormatException and NullReferenceException, which we saw in the lesson. Truth be told, just using the basic Exception class does the trick in many if not most situations as well.
Also note that exceptions can also be thrown from inside catch blocks can be useful:
static void DoStuff()
{
try
{
// Some operation
}
catch (Exception ex)
{
if (something)
{
// I'm showing a possibility here... but I don't recommend this pattern in many cases
throw new Exception("Something happened in DoStuff");
}
else
{
throw ex;
}
}
}
Note that in most situations the example above is not desirable because it hides the real problem if you choose to throw your own (rather undescriptive) exception instead. Also note that you can choose to throw an existing exception. In particular, throwing a custom Exception within a catch block hides some things such as the stack trace, which gives you information about the path taken to get to the error. You can actually include info like this (as well as the Exception that was thrown originally) by including the Exception object as a param in the constructor:
catch (Exception ex)
{
throw new Exception("Something happened in DoStuff", ex);
}
This is an acceptable way to "wrap" an error in some code you wrote by including additional information or a custom error message. The exception passed as a parameter is available to whatever catches this Exception via the "InnerException" property, which itself is an Exception class which can have it's own InnerException, etc etc.. I will discuss this more in future lessons, possibly in the advanced series.