The first line shows the method that threw the exception and the smallest line – the Main() method (note that the Main() method may not be present in the case of an exception thrown by a thread which is not the main thread of the program). By definition, calling this method for every exception within an exception chain will always return the same result: the first exception thrown.
Application vs. System Exceptions
In most cases, the code that throws the exception throws the message in the constructor. The StackTrace property returns information for the entire stack contained in the exception (we've already seen what this information looks like).
Throwing and Catching Exceptions
Nested Exceptions
How to Read the Stack Trace with Nested Exceptions?
After each exception type, we can see the message for the respective exception (as contained in the message property). Using the information in the stack trace (the file name, the method and the line number), we can find out how and where the exceptions occurred.
Visualizing Exceptions
In web applications, errors often appear at the top or bottom of the page or next to the UI field associated with the error. In GUI applications, we need to display errors in a dialog window that contains a user-friendly description of the error. As you can see, there is no single 'right' way to handle and visualize exceptions as it depends on the type of application and its intended audience.
Still, there are some recommendations on how to handle exceptions and what is the best way to show them to the users.
Which Exceptions to Handle and Which Not?
A method is competent to handle an exception if it expects this exception, the method has the information why the exception was thrown and what to do in this situation. If we have a method that needs to read a text file and return its contents as a string, in this case that method might catch FileNotFoundException and return an empty string. So apparently the method is not competent to handle such an exception and therefore the best course of action is to pass the exception to the calling method so that it can (hopefully) be handled at another level by a method capable of doing so is authorized.
Throwing Exceptions from the Main() Method – Example
Catching Exceptions at Different Levels – Example
In the Main() method, we only handle exceptions of type IOException and let the CLR handle all other exceptions (for example, if an OutOfMemoryException is thrown during program execution, it will be handled by the CLR). If the Main() method passes an incorrect file name, a FileNotFoundException will be thrown in ReadFile() during TextReader initialization. If, on the other hand, the file exists but there is a problem reading it (insufficient permissions, corrupted file contents, etc.), the appropriate exception that will be thrown will be handled in the Main() method.
Exception handling at different levels allows error conditions to be handled in the most appropriate place for that particular error.
The try-finally Construct
When Should We Use try-finally?
Resource Cleanup – Defining the Problem
A return statement could be executed before closing the reader (in our trivial example this would be obvious, but it is not always so obvious). So our method, as described in the example above, has a critical flaw: it closes the reader only in the last scenario. In all other cases, the code that closes the reader is not executed.
Resource Cleanup – Solving the Problem
The code above guarantees that if the file is opened, it will be closed regardless of how the method exits. The example above should in principle properly handle all exceptions related to opening and initializing the browser (such as FileNotFoundException).
Resource Cleanup – Better Solution
Multiple Resources Cleanup
For our example, we chose file streams to release resources, but the same principle applies to any resource that requires proper cleanup. The second approach is slightly more risky, because if an exception occurs in the finally block, some resources will not be cleaned up. In the example above, if an exception is thrown during r1.Release(), r2 will not be cleared.
If we use the first option, there is no such problem, but the code is a bit longer.
IDisposable and the "using" Statement
IDisposable
The Keyword "using"
Nested "using" Statements
When a class implements IDisposable interface, it means that the creator of this class expects that it can be used with the using statement and the class contains an expensive resource that should not be unreleased. The implementation of IDisposable also means that it must be released immediately after we are done using the class and the easiest way to do this in C# is with the use of statement.
Advantages of Using Exceptions
Separation of the Exception Handling Code
Using error codes is a standard way of handling errors in procedure-oriented programming, where each method returns an int, which provides information about whether the method executed correctly. Errors have no type, description, or stack trace, and we have to wonder what the various error codes mean. In fact, exceptions do not save us the effort of finding and processing errors, but they give us a more elegant, concise, clear and efficient way to do it.
Grouping Different Error Types
Catching Exceptions at the Most Appropriate Place
First in Method1() we need to parse the error code returned by the ReadFile() method and finally pass it to Method2(). In Method2() we need to parse the error code returned by Method1() and finally pass it to Method3() where we need to handle the error itself. Let's remember that the CLR looks for exceptions back in the call stack of the methods and lets each of them define the catching and handling of the exceptions.
If an error occurs while reading the file, it will be ignored in Method1() and Method2() and will be caught and handled in Method3(), where the most appropriate place to handle the error is.
Best Practices when Using Exceptions
When to Rely on Exceptions?
It is not a good practice to rely on exceptions for expected events for another reason: performance. An object must be created to hold the exception, the stack trace must be initialized, and the handler for this exception must be found, and so on.
Throw Exceptions to the End User?
This dialogue is very suitable for e.g. developers or administrators, but it is highly inappropriate for end users. The message is easy to understand from the user and also contains technical details which can be used if needed but which are not visible at the beginning. It is recommended when exceptions are not caught by anyone (such exceptions can only be runtime errors) to be caught by a global exception handler, which stores them on disk and displays user-friendly message such as "An error occurred, please try again later".
It is a good practice to display not only a user-friendly message, but also technical information (stack trace) that is available on request (e.g. via an additional button or link).
Throw Exceptions at the Appropriate Level of Abstraction!
If Your Exception Has a Source, Use It!
Give a Detailed Descriptive Error Message!
If IndexOutOfRangeException is thrown, it is important to indicate in the error message the index that cannot be reached. If we don't know the position, we will hardly understand why we are outside the array. If we have a row in the file without an integer, we should get an error, which explains that, for example, in row 17 an integer is expected instead of a string (and the string is printed).
If we find an error in the expression, the exception should say what error occurred and at what position.
Error Messages with Wrong Content
Use English for All Exception Messages
The code causing the error may use String.Format. to build the error message.
Never Ignore the Exceptions You Catch!
Dump the Error Messages in Extreme Cases Only!
In case of incorrect input, the method should throw an exception and should not return an incorrect result. If the method cannot do the job it was designed to do, it should throw an exception. For example, if we try to retrieve a substring from index 7 to 12 from a string of length 10, it should throw an exception and not return fewer characters.
We will give another example, which confirms the rule that a method must do the job for which it was created, or throw an exception.
Don’t Catch All Exceptions!
If we can't give a suitable name to the method, it means that it does many things and we should split it so that everything is in separate method. If the array is empty, the method must either return an empty array or return an error. It can happen that there is not enough space on the flash drive and the file cannot be copied.
From the user's point of view, the only correct behavior of the program is the last one: if a problem occurs, the file must not be partially copied and an error message must be displayed.
Only Catch Exceptions You Know How to Process!
The Catch block catches all exceptions (regardless of their type), not just FileNotFoundException, and in all cases prints that the file is not found. There are unexpected situations, such as when the file is locked by another process in the operating system. In such cases, the CLR will generate UnauthorizedAccessException, but the message that the program will display to the user will be incorrect and misleading.
The same will happen when we run out of memory while opening a file and an OutOfMemoryException is generated.
Exercises
Write a method ReadNumber(int start, int end) that reads an integer from the console in the range [start...end]. Write a method that takes the name of a text file as a parameter, reads the file, and returns its contents as a string. Write a method that takes the name of a binary file as a parameter, reads the contents of the file, and returns it as an array of bytes.
Write a program that gets the full path to a file from the user (for example, C:\Windows\win.ini), reads the contents of the file, and prints them to the console.
Solutions and Guidelines
Write a program that takes a positive integer from the console and prints the square root of this integer. In case the input integer is not valid or it is not in the required range, throw appropriate exception. The exception must contain the name of the processed file and the number of the row where the problem occurred.
Ensure that all possible exceptions are caught and a user-friendly message is printed on the console.