.NET

Unexpected Errors in Managed Applications

Jason Clark

Code download available at:NET0406.exe(123 KB)

Contents

How .NET Deals with Unhandled Exceptions
Exceptions and Code Access Security
Some Best Practices
Enforcing Best Practices with FxCop

In this installment of the .NET column I am going to present a handful of tips for dealing with unexpected errors. While it is common practice to catch exceptions to deal with expected failure cases, it is also common for managed applications to encounter errors for which the application is completely unprepared. In an ideal world you will establish clear policies for how your application discovers and responds to unhandled exceptions, reduced security permissions, and other related edge-cases, but in practice these are often overlooked.

Error handling infrastructure is a deceptively complex topic, and you can easily be overwhelmed by details, so I'll address a few relatively simple operations that are broadly applicable for dealing with unexpected errors.

How .NET Deals with Unhandled Exceptions

The common language runtime (CLR) has a set of rules on managing unhandled exceptions. But if you are using the Windows® Forms classes, the Windows Forms message pump applies its own unhandled exception policy, which differs from the policy of the CLR. Add to that the fact that .NET Framework hosts such as ASP.NET impose their own standards on unhandled exceptions, and you can see where the topic begins to get complicated. Still, it's important that managed applications take deliberate steps to deal with unhandled exceptions.

To help you understand unhandled exceptions, let's first take a brief look at handled exceptions. The code in Figure 1 shows two methods, one that calls the other. Admittedly, this code is strange, but it conveys a point. When MethodA calls MethodB, MethodB throws an exception of type InvalidOperationException. This causes normal execution to stop at the point of the thrown operation, and the execution engine searches up the call stack for a compatible catch handler. Because there is a catch clause in MethodA that handles this type of exception, the execution engine finds the compatible catch clause in MethodA and executes it. Once the catch block is completed, MethodA continues running normally, starting with the code following the try/catch block. This is an example of a handled exception; it is indicative of the type of exception handling that most managed applications must perform in response to expected error cases when calling into a class library and managed application code.

Figure 1 Handling Exceptions

void MethodA() { try { MethodB(1); } catch (InvalidOperationException e) { // place handling code here } } void MethodB(Int32 num) { if (num == 1) { throw new InvalidOperationException(); } else { throw new ArgumentException(); } }

However, if MethodA called MethodB with an argument of any value not equal to 1, MethodB would throw an ArgumentException, for which MethodA does not provide a handler. Now if none of the methods further up in the call stack provide an ArgumentException-compatible handler either, then that exception is an unhandled exception.

The code in Figure 2 throws an unhandled exception in response to the user pressing any of its five buttons. Each button represents a different threading context from which an exception can go unhandled. To get a feel for how managed code responds to unhandled exceptions, compile the code in Figure 2 as a console application, run it (but not under the debugger), and start pressing buttons here and there.

Figure 2 ThrowsUnhandledExceptions.cs

using System; using System.Threading; using System.Drawing; using System.Windows.Forms; class ExceptionForm : Form { public ExceptionForm() { String[] buttonStrings = { "Throw From This Button's Event Handler", "Close Window and Throw After the Fact", "Create a Thread and Throw From It", "Throw From a Thread-pool Thread", "Throw From a Finalizer Method" }; for (Int32 index = 0; index < buttonStrings.Length; index++) { Button button = new Button(); button.Text = buttonStrings[index]; button.Size = new Size(224, 24); button.Location = new Point(8, 11 + index * 32); button.Click += new EventHandler(OnButtonClick); Controls.Add(button); } ClientSize = new Size(240, 174); Text = "Exception Form"; } private void OnButtonClick(Object sender, EventArgs args) { switch (((Button)sender).Text) { case "Throw From This Button's Event Handler": throw new InvalidOperationException(); case "Close Window and Throw After the Fact": App.shouldThrowOnExit = true; Close(); break; case "Create a Thread and Throw From It": Thread t = new Thread(new ThreadStart(ThreadMethod)); t.Start(); break; case "Throw From a Thread-pool Thread": ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadPoolMethod)); break; case "Throw From a Finalizer Method": CreateObjectThatThrowsOnFinalize(); GC.Collect(); GC.WaitForPendingFinalizers(); break; } } void ThreadMethod() { throw new InvalidOperationException(); } void ThreadPoolMethod(Object param) { throw new InvalidOperationException(); } void CreateObjectThatThrowsOnFinalize() { new ThrowsOnFinalize(); } class ThrowsOnFinalize { ~ThrowsOnFinalize() { throw new InvalidOperationException(); } } } class App { internal static Boolean shouldThrowOnExit = false; public static void Main() { Application.Run(new ExceptionForm()); if (shouldThrowOnExit) throw new InvalidOperationException(); } }

The behavior represented in this application is surprising the first time you run across it. In particular, you might notice that pressing any of the bottom three buttons seems to have no effect on the application whatsoever, even though they are causing unhandled exceptions to be thrown! In certain cases, exceptions that are unhandled are swallowed whole by the CLR (although this behavior is changing in the .NET Framework 2.0). Here is a summary of the unhandled exception handler default behaviors that are executing in this application.

  • Unhandled exceptions that occur on the application's main thread cause the application to terminate.
  • Unhandled exceptions that occur in threads other than the application's main thread are swallowed by the CLR. This includes manual threads, thread pool threads, and the CLR's finalizer thread. If your application is a console application, the CLR outputs exception text to the console (though your application keeps running). If your application is not a console application, there is no outward indication when an exception occurs and your application keeps running.
  • Unhandled exceptions that occur on a thread that is pumping window messages via the Windows Forms classes are subject to the Windows Forms unhandled exception handler. A debug dialog box is produced by default, but this behavior can be overridden (more on this in a moment).

Now let's take a look at what applications should do, in abstract terms, in the event of an unhandled exception.

In an attempt to reduce the number of unhandled exceptions in their applications, many developers catch the base Exception type, like I've done in the following snippet:

try { ••• } catch (Exception e) { // handle any CLS-compliant exception }

Counter to what you may think, code like this is almost always undesirable and actually reduces the robustness of the application. Here's why: there are only a miniscule number of cases in which your code can be sure that it knows how to appropriately handle any possible exception that escapes a try block. There are far more unexpected exception cases than you might imagine. In managed code, even code that doesn't call any methods could throw a variety of exceptions, including security exceptions, out-of-memory exceptions, execution-engine exceptions, and the like. It is best for your applications to always catch specific exceptions and leave all other unexpected exceptions as unhandled. I will look at this concept later in this column.

When an exception goes unhandled, it is a good rule of thumb to shut down the application. An unhandled exception is a bug in your application, and continuing to execute in an undefined state runs the risk of data corruption and other ugly problems. Certain server-side applications are sophisticated enough to unload only the state and threads for a particular client in the event of unhandled exceptions. But most client-side applications, and many server-side applications, are not as sophisticated and the process should be shut down (and perhaps automatically restarted).

Of course, this means that your application cannot live with the default unhandled exception behavior exhibited by the application in Figure 2. This behavior leaves a fair number of unhandled exceptions ignored by the application, and in one of the cases a dialog box that is guaranteed to mystify the user is displayed. Fortunately, very little code is required to improve things dramatically.

The code in Figure 3 shows a boilerplate implementation of the Main entry point that sets up an unhandled exception handler event for both the CLR unhandled exception handler and the Windows Forms unhandled exception handler. This is done by providing handlers for the AppDomain.UnhandledException event and the Application.ThreadException event.

Figure 3 Handling Unhandled Exceptions

class App { public static void Main() { try { SubMain(); } catch (Exception e) { HandleUnhandledException(e); } } public static void SubMain() { // Setup unhandled exception handlers AppDomain.CurrentDomain.UnhandledException += // CLR new UnhandledExceptionEventHandler(OnUnhandledException); Application.ThreadException += // Windows Forms new System.Threading.ThreadExceptionEventHandler( OnGuiUnhandedException); // Start application logic // Perhaps call to Application.Run(...); } // CLR unhandled exception private static void OnUnhandledException(Object sender, UnhandledExceptionEventArgs e) { HandleUnhandledException(e.ExceptionObject); } // Windows Forms unhandled exception private static void OnGuiUnhandedException(Object sender, ThreadExceptionEventArgs e) { HandleUnhandledException(e.Exception); } static void HandleUnhandledException(Object o) { Exception e = o as Exception; if (e != null) { // Report System.Exception info Debug.WriteLine("Exception = " + e.GetType()); Debug.WriteLine("Message = " + e.Message); Debug.WriteLine("FullText = " + e.ToString()); } else { // Report exception Object info Debug.WriteLine("Exception = " + o.GetType()); Debug.WriteLine("FullText = " + o.ToString()); } MessageBox.Show("An unhandled exception occurred " + "and the application is shutting down."); Environment.Exit(1); // Shutting down } }

Notice that in this example the exception information is only reported as debug output. However, for a shipping application it would make more sense to report the error information to an event log, a file, or to isolated storage. The companion code to this column includes an implementation of the application in Figure 2 that reports its unhandled exception data to a file (the download is available on the MSDN® Magazine Web site).

In addition to reporting failure information in such a way that you can fix the bug in future releases of your software, using unhandled exception notifications provides a way to make your application behave the same in the event of any unhandled exception. This improves the determinism and robustness of your applications.

Note that for ASP.NET applications the analog to the Windows Forms Application.ThreadException event is the HttpApplication.Error event. You can define a handler for the event in your Global.asax file by defining an Application_Error method, or you may register for the event directly in code. ASP.NET applications have an additional feature that redirects to a page in the case of an unhandled exception. To set this to a page other than the default, you may specify a <customErrors defaultRedirect="..." /> element and attribute in your web.config file.

Exceptions and Code Access Security

All managed applications can be subject to a security sandbox known as Code Access Security (CAS). The idea behind CAS is that your code can execute as partially trusted, which means that the CLR's just-in-time compiler, verifier, and class library impose certain restrictions on the actions that your code can perform.

The CAS features of the CLR are quite powerful, and for cases in which you want to locally execute code that you don't trust, it is great. But many developers aren't aware of just how easy it is to bump into CAS restrictions when you don't expect them. It surprises me how little the tools and documentation call out this aspect of managed code. Let's look at an example:

class App { public static void Main() { String[] entries = Directory.GetFileSystemEntries(@"C:\"); foreach (String s in entries) Console.WriteLine(s); } }

This code lists the names of all of the files and subdirectories in the root of your C: drive. Compile and run this code as a console application, and you should see a list of directory entries. Now copy the executable file (no need to rebuild) to a share somewhere on your network, and run the application. You should see output to your console that looks something like Figure 4.

Figure 4 Security Exception Console Output

Unhandled Exception: System.Security.SecurityException: Request for the permission of type System.Security.Permissions.FileIOPermission, mscorlib, Version=1.0.5 000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 failed. at System.Security.CodeAccessSecurityEngine.CheckHelper(PermissionSet grantedSet, PermissionSet deniedSet, CodeAccessPermission demand, PermissionToken permToken) at System.Security.CodeAccessSecurityEngine.Check(PermissionToken permToken, CodeAccessPermission demand, StackCrawlMark& stackMark, Int32 checkFrames, Int32 unrestrictedOverride) at System.Security.CodeAccessSecurityEngine.Check(CodeAccessPermission cap, StackCrawlMark& stackMark) at System.Security.CodeAccessPermission.Demand() at System.IO.Directory.GetFileSystemEntries(String path, String searchPattern) at System.IO.Directory.GetFileSystemEntries(String path) at App.Main() The state of the failed permission was: <IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" PathDiscovery="C:\."/>

The most immediately useful information in this exception output are the first four lines which state that "Request for the permission of type ... FileIOPermission ... failed" and the last line which shows the resource to which the code was denied access: "C:\."

What surprises many developers is that just copying their app to another location in the network causes the app—which was previously working just fine—to crash! No code or system configuration changes are required to see this behavior. This is because the default security policy of the CLR is to grant only partial trust to any code that originates from a network location. In other words, by default your application is only fully trusted if it originates from a hard drive location in your local system. When partially trusted code attempts an action that it is not permitted to perform, the results are a SecurityException.

If this information is new to you, it can be a useful learning experience to copy one of your working applications to a network share and try launching it. Unless your application deliberately addresses CAS, there will likely be problems due to security exceptions. Finding out which kinds of actions are not permitted for your own partially trusted code can be very educational.

In the future, it is likely that Windows, the CLR, and developer tools will make more direct use of the sandbox features of the CLR (and perhaps your application is one of the few that deliberately utilizes the sandbox today). However, most managed client applications and many managed Web applications will not function correctly unless they run with full trust.

If your application requires full trust to run, then as one of its startup actions you might consider testing to see if it is granted full trust. Doing so will provide your code the opportunity to alert the user to the configuration or installation problem, and shut down gracefully. This is better than subjecting the user to the default behavior—a flurry of security exceptions, one of which will probably activate the unhandled exception behavior.

The code in Figure 5 modifies the preceding example to first test for permissions, and then perform the application logic. The gist of this code is that a System.Security.PermissionSet object is created in such a way as to represent an unrestricted set of permissions. Then the code calls the Demand method on the permission set, which results in a SecurityException if the executing code (or any other code in the call stack) is not granted these permissions. This is perhaps the simplest policy that your application can employ to require full-trust execution while allowing the application to report failure to the user.

Figure 5 Check for Full Trust Permissions

class App { public static void Main() { try { // Demand full trust permissions PermissionSet fullTrust = new PermissionSet(PermissionState.Unrestricted); fullTrust.Demand(); // Perform normal application logic String[] entries = Directory.GetFileSystemEntries(@"C:\"); foreach (String s in entries) Console.WriteLine(s); // Report that permissions were not full trust } catch (SecurityException) { Console.WriteLine("This application requires full-trust " + "security permissions to execute."); } } }

Other alternatives to this approach exist, but they are more complicated. For example, you can find all of the places in your application where security exceptions occur and wrap them in try/catch blocks. This option results in code that can be run from more security contexts. However, the downside is that this approach requires analysis for each failure case to determine what kind of backup action to perform when the ideal action isn't permitted by the sandbox.

Another option is to use a feature of CAS called declarative security, which will cause the runtime to fail to load your code if it is not sufficiently permitted to perform its duties. Of course, this option will not allow your code to report its shortcomings to the user because the CLR will refuse to even load the code. That ends up looking like an application crash to the user.

In practice, today's .NET-based applications fall into two categories: those that have a specific use for partially trusted execution, and those that don't. In the cases where partial trust provides benefits to your user, the many features of CAS can be useful, including declarative security, catching of security exceptions, testing for specific permissions using SecurityManager.IsGranted, and so forth. However, if your application's scenario does not require or benefit from CAS, then the simple test for permissions on application startup gives your user an experience that's much better than an application crash, and your code investment is minimal. In addition, Visual Studio® 2005 will include a tool called PermCalc which will help determine which permissions are required for your application to run successfully.

Some Best Practices

Exception handling best practices and design is a topic that generates lots of debate, both inside Microsoft and throughout the software industry. I may one day distill into a column some of the best practices that we have captured on the Microsoft CLR team regarding effective use of exception handling in managed code. Space does not permit full coverage of exceptions best practices here. However, there are two best practices that enjoy wide acceptance and provide significant bang for your buck. Plus, they relate nicely to the use of unhandled exception handlers in your apps:

  • Don't catch or throw base exception types
  • Use finally blocks liberally

I touched on this first guideline earlier by stating that you should not catch the base exception type System.Exception. Now that I have looked at unhandled exceptions, let's rethink that.

It is natural for developers to make the following mental progression when thinking about that:

  1. Exceptions represent failures
  2. Catching the base exception type increases the likelihood of catching a failure to nearly 100 percent
  3. Catching the base exception is good

The reason that this logic does not follow is that catching an exception is not fundamentally the same thing as handling a failure. Catching specific exception types makes it far more likely that the developer will be able to write appropriate handling code. Catching base exceptions, though, has the effect of hiding unexpected failures, while very likely failing to cover all of the bases that it should in terms of handling logic.

There is plenty of historical data with managed code to back up the guidance that you should allow unexpected exceptions to propagate up the call stack for most situations. Meanwhile, with a proper unhandled exception-handling policy, you will learn of these unexpected cases from your exception logs. Catching too aggressively masks the ability to learn of the failures that were unexpected (but are now expected).

Now let's look at the guideline surrounding finally blocks. In general, well-written managed code contains more try/finally constructs than it does try/catch constructs. The reason for this is that finally blocks guarantee the ability to perform method-level clean-up operations in both exceptional and non-exceptional executions. Many methods include code that qualifies as "clean-up operations." This includes the closing of files and other resources that were opened in the method, unlocking synchronization locks that were taken during the method, and so forth. Meanwhile, if you follow the first guideline—only catching expected exceptions—the number of methods that actually catch any exceptions at all is reduced in the end.

These two guidelines go hand in hand. By not over-catching, you increase the likelihood that exceptions will travel further up the call stack in your applications. Sometimes these exceptions will go unhandled; other times code further up the stack will catch the exception. Either way, liberal use of finally blocks reduces the chaos in your application when exceptions travel up your call stack.

Enforcing Best Practices with FxCop

While I am on the topic of best practices, if you are not already familiar with a tool called FxCop, you probably should be. It's useful and free online at https://www.microsoft.com/en-us/download/details.aspx?id=6544.

The CLR team at Microsoft maintains a set of guidelines and best practices for writing managed code libraries. They also maintain the FxCop tool, which analyzes managed assemblies for guideline and best-practice violations. Most of the teams at Microsoft writing production managed code use FxCop, and many include it as part of the daily build process. Microsoft routinely releases updates of FxCop which include new useful rules.

Like any analysis tool, FxCop is not a substitute for human intelligence. However, it does help you identify places where you code violates specific guidelines. For example, FxCop will tell you where you catch System.Exception. FxCop guidelines are factored into an extensible mechanism called a rule. The tool lets you enable and disable rules independently of one another on a per-analysis bases.

FxCop also supports the creation of custom rules (see the Bugslayer column by John Robbins in this issue for a detailed look at creating custom rules). Some of the rules enforced by the tool (such as naming conventions) may seem unnecessarily strict, but remember that Microsoft developers use this tool internally to promote the consistency of their class libraries (a benefit to all developers using managed code). I suggest that you disable the rules that don't benefit your project. But I do believe that taking a couple of hours to investigate all the uses of FxCop will pay off in your managed projects.

Send your questions and comments for Jason to  dot-net@microsoft.com.

Jason Clark provides training and consulting for Microsoft and Wintellect and is a former developer on the Windows NT and Windows 2000 Server team. He is the coauthor of Programming Server-side Applications for Microsoft Windows 2000 (Microsoft Press, 2000). You can get in touch with Jason at JClark@Wintellect.com.