|
|
|
In the "old days" of programming, error handling was a nightmare. Anytime there was a chance that something could go wrong, the programmer would have to write ways of handling each and every bad scenario. And that's when the programmer actually bothered to do so. Quite often, many possibilities would go unchecked, either because the programmer forgot, or because he simply didn't feel like writing 3000 lines of code every time there was a chance that something could fail. What if the user enters a bad number? What if they hit the wrong button? What if a calculation fails? What if the keyboard breaks? What if the computer runs out of memory? What if the processor is struck by a comet? In short, the programmer's code was completely cluttered with all manner of error checks and fallbacks, to the point where the actual program logic was buried in a huge mess of error-handling routines, and it was impossible to tell what the program actually did. Today, we like to take the optimistic approach. We assume that everything will run fine, but if it doesn't, we provide a simple contingency plan off to the side, out of the way of our main logic. The whole point of this is that we don't want our program's fundamental logic to be cluttered with handling what are basically exceptions. How is this accomplished? Through try/catch blocks. Observe this simple example... try { Console.Write("Enter a number: "); int number = Convert.ToInt32(Console.ReadLine()); Console.WriteLine("This code executed because everything went fine!"); } catch { Console.WriteLine("This code executed because there was an error!"); } The code in the "try" part of the try/catch is the code you want to run, under normal circumstances. This is our "fundamental logic"; it's how the program is supposed to work when everything goes smoothly. The catch, as you might expect, is thus the code to run when things do not run smoothly. The instant that an error is encountered in the "try" block (the sort of error that would cause a program crash), the program stops running the try block, and immediately jumps down to the "catch block", running that instead. If the try block completes without errors, it skips the "catch" block and moves on to the rest of your code. So, remember those program crashes you were getting when your program tried to convert "afafafaf" into a number, or when you tried to access element -2919 in an array? That's what's called an "unhandled exception". It means that something really bad has happened, and unless it gets handled somewhere (by a try/catch), it's going to make your program crash. Period. So pretty much any code in your program that has any chance of making your program crash should be covered by a try/catch. What do you put in the "catch" block? It depends on the nature of the problem. If jumping down to the "catch" means that most of your program has been skipped, you probably don't have to do anything more than write a message to the console, explaining that there was an error. If it's an error that was picked up when asking the user for input, you can scold the user, and demand that that they follow your instructions more carefully. If it's a fatal error that renders the rest of your program meaningless, you might just quit while you're ahead and use "return;" to jump right out of main and end the program (more on "return" later). You can put any code you want in the catch, including no code at all. It's basically a question of what kind of problem you've encountered, and how much effort you want to spend fixing it. Just remember that if something you write in the catch block causes an exception, and it's not caught in another catch block, your program will crash, anyway! This brings us to another (optional) part of the try/catch is the "finally". It works like this: try { Console.Write("Enter a number: "); int number = Convert.ToInt32(Console.ReadLine()); Console.WriteLine("This code executed because everything went fine!"); } catch { Console.WriteLine("This code executed because there was an error!"); } finally { Console.WriteLine("This code executes whether there's an error or not!"); } Any code in the "finally" block will always be executed, regardless of whether an exception was encountered. Even if an unhandled exception is generated in the "catch" block which makes the program crash, the code in the "finally" block will be executed before the program crashes! In this manner, you can put your program's "normal" logic into the "try" block, your "error handling/recovery" logic in the "catch" block, and your "mandatory cleanup" logic into the "finally" block. For example, you might use the "finally" block to free up resources or file handles that your program had allocated earlier (we don't want those to be left floating around after the program crashes). When an error is encountered, a special object known as an Exception is generated, which is then passed down to the "catch" block (hence the term "unhandled exception", which occurs when there is no "catch" block to receive the exception). An Exception contains extensive information on the error, including a message that details the cause of the error, a description of where the error occurred, and even the line number in your code that caused the problem! In order to use this information, we add a small piece of code to the "catch" statement: try { // normal logic goes here } catch(Exception ex) { Console.WriteLine("Message:\t" + ex.Message); Console.WriteLine("Source:\t\t" + ex.Source); Console.WriteLine("TargetSite:\t" + ex.TargetSite); Console.WriteLine("StackTrace:\t" + ex.StackTrace); } The brings a new variable into being, "ex" (you can declare it with any name you want), which contains all of the Exception's information. The above code uses WriteLine() to display four parts of this information: 1) Message; a description of what caused the error. 2) Source; the namespace that the error occurred in. 3) TargetSite; the "method" that the error occurred in. 4) StackTrace; the exact location in your code where the problem occurred. The last three probably won't mean much to you right now (except for the fourth one, which gives you the line number if your program compiles with debug information), but the first one comes in handy all the time. Another interesting point is that we can have multiple "catch" statements for each "try". This comes in handy when we might encounter several different types of Exception in the "try" block, and we want to react differently for each possible type of Exception... try { } catch(FormatException ex) { Console.WriteLine("Caught a FormatException: " + ex.Message); } catch(DivideByZeroException ex) { Console.WriteLine("Caught a DivideByZeroException: " + ex.Message); } catch(IndexOutOfRangeException ex) { Console.WriteLine("Caught a IndexOutOfRangeException: " + ex.Message); } So we could potentially encounter three different kinds of Exceptions in the "try" block, and we'd be able to handle them all differently. An added beenfit is that specific exceptions tend to offer more specialized information, such as the actual string that couldn't be converted to an integer. A word of warning, though... if you don't include the generic, plain old "Exception" in one of your "catch" blocks, and an Exception is thrown which you didn't specifically mention, it won't be caught, and your program will explode. So it's a good practice to always include the generic case as a "catch all" for anything you didn't mention specifically. Nor do you have to write a separate catch for every possible Exception type. If you don't care about specific errors, and you just want some basic exception coverage, then the generic one will work fine. Just in case you want to get specific, however, here are a few of the more common exceptions you might try catching: - Exception, your "catch all" kind of error. - FormatException, which covers stuff like trying to use Convert.Int32 on a string like "meow". - DivideByZeroException, which obviously occurs when you divide by zero. - IndexOutOfRangeException, which covers stuff like trying to access element 5 in an array that's four elements long. - NullReferenceException, which happens when you try to do stuff with or to a variable that has a value of null. By the way, there's no need to wait for the system to throw an error. If we encounter something during the execution of our program that's really going to mess things up, we can actually throw one ourselves. It works something like this: throw new Exception("I didn't want to look at the user's face for one second longer!"); And the really cool thing is, we can put whatever message we want in the exception. And when it's caught in the "catch" block and we display ex.Message, we'll get the same message you entered when you threw the exception! I know what you're thinking. "Why the heck would I want a piece of code that forces my program to crash?" It's a good question, and the simple answer is safety. There's an old saying that a wounded program causes far more damage than a dead one. If your program does something important, like managing a database, and it detects that something weird is going on inside of itself, there should be a "throw new Exception" line somewhere that allows it to "self-destruct" before it can damage the database, or present misleading information. Of course, ideally, you'll be able to catch your own exception and handle it gracefully, not crashing at all. But always remember: a program that misleads or damages is much, much worse than a program that crashes. When in doubt, pull the plug. This brings us home to our final point in this lesson... you don't have to handle every error by Exception. Some stuff can be handled with a simple "if" statement... try { int[] nums = new int[] {5, 7, 4, 8}; Console.Write(Environment.NewLine + "Enter an index to view, or -1 to quit: "); int index = Convert.ToInt32(Console.ReadLine()); if( index >= nums.Length ) Console.WriteLine("Index {0} is out of range!", index); else if( index > -1 ) Console.WriteLine("Index {0} = {1}", index, nums[index]); } catch(Exception ex) { Console.WriteLine("Caught an Exception: " + ex.Message); } In the above case, we're actually handling the mostly likely cause of error (the user enters an index that's out of range) with an "if". We simply check to see if the value the user entered is out of range before using it, thus avoiding the need to mess around with Exceptions. Of course, the whole thing's still inside a try/catch, in case something else goes off the rails. Why bother using "if" statements, when you could just catch every possible error with try/catch? For one thing, when an Exception is generated, there's often a noticeable delay (this is your program scrambling to gather Exception information, and maybe clean up some of the mess that it's made "under the hood"). If you don't think that the delay could be that bad, go into Windows Explorer and see what happens when you try to delete a file that's currently in use. So, it sometimes pays to "intercept" things before they get that far, and avoid the error to begin with. For another thing, exceptions take over the flow of your program. If you've got a nice little "while" loop going in your "try" block, you're going to be yanked away from it once an Exception is encountered and the program jumps down to your "catch". So once again, it sometimes pays to "intercept" things before they get that far, and make your program's flow more predictable. Having said that, why do we actually use Exceptions? Mainly, because it's impossible to think of and write code to handle every possible thing that could go wrong during the course of an ordinary application, and quite frankly, we don't want to. So Exceptions make for a wonderful "catch all" that let us handle unexpected errors. That is... Exceptional errors. The very name, "Exception", says it all. A few well-placed "if" statements can make our program faster and more predictable, but three hundred "if" statements that try to account for everything will kill it. Use "if"'s to handle the most likely ways your program can go wrong, and let the try/catch handle everything else. Doing so will keep your code clean, clear, stable and efficient. Now onto your code sample... using System; namespace E_TryCatch { class Class1 { [STAThread] static void Main(string[] args) { // Basic try/catch/finally try { Console.Write("Enter a number: "); int number = Convert.ToInt32(Console.ReadLine()); Console.WriteLine("This code executed because everything went fine!"); } catch { Console.WriteLine("This code executed because there was an error!"); } finally { Console.WriteLine("This code executes whether there's an error or not!"); } Console.Write(Environment.NewLine); // Using the Exception class try { int zero = 0; int answer = 5 / zero; // dividing by zero will create an exception } catch(Exception ex) { Console.WriteLine("Exception caught!"); Console.WriteLine("Message:\t" + ex.Message); Console.WriteLine("Source:\t\t" + ex.Source); Console.WriteLine("TargetSite:\t" + ex.TargetSite); Console.WriteLine("StackTrace:\t" + ex.StackTrace); } Console.Write(Environment.NewLine); // Catching different exceptions try { // Might throw FormatException Console.Write("Enter a number: "); int number = Convert.ToInt32(Console.ReadLine()); // Might throw DivideByZeroException Console.Write("Enter a number to divide it by: "); int divisor = Convert.ToInt32(Console.ReadLine()); int answer = number / divisor; // Might throw IndexOutOfRangeException int[] nums = new int[] {2, 3}; Console.Write("Enter an index to view (0 or 1): "); int index = Convert.ToInt32(Console.ReadLine()); Console.WriteLine("Value is " + nums[index]); // Might throw NullReferenceException // But we don't have a specific case for that, so it's // caught in the generic catch... string hello = null; string helloUpperCase = hello.ToUpper(); } catch(FormatException ex) { Console.WriteLine("Caught a FormatException: " + ex.Message); } catch(DivideByZeroException ex) { Console.WriteLine("Caught a DivideByZeroException: " + ex.Message); } catch(IndexOutOfRangeException ex) { Console.WriteLine("Caught a IndexOutOfRangeException: " + ex.Message); } catch(Exception ex) { Console.WriteLine("Caught generic Exception: " + ex.Message); } Console.Write(Environment.NewLine); // We can throw our own exceptions, too... try { Console.Write("Please enter your name: "); string name = Console.ReadLine(); throw new Exception("That name's so stupid that I'm throwing an error!"); } catch(Exception ex) { Console.WriteLine("Caught generic Exception: " + ex.Message); } // Use "if"'s to handle the most likely problems, and let try/catch // handle everything else try { int[] nums = new int[] {5, 7, 4, 8}; int index = 0; while( index > -1 ) { Console.Write(Environment.NewLine + "Enter an index to view, or -1 to quit: "); index = Convert.ToInt32(Console.ReadLine()); if( index >= nums.Length ) Console.WriteLine("Index {0} is out of range!", index); else if( index > -1 ) Console.WriteLine("Index {0} = {1}", index, nums[index]); } } catch(Exception ex) { Console.WriteLine("Caught generic Exception: " + ex.Message); } // Prompt for exit Console.Write(Environment.NewLine + "Program complete! Hit enter to exit..."); Console.ReadLine(); } } } |
|