BT

New Early adopter or innovator? InfoQ has been working on some new features for you. Learn more

Designing with Exceptions in .NET

| Posted by Jonathan Allen Follow 34 Followers on Sep 09, 2016. Estimated reading time: 12 minutes |

Key takeaways

  • Prefer built-in exceptions, or subclasses thereof, for common types of errors.
  • Use the type of exception to indicate whether the error is in the application itself, in the library being called, or an environmental issue.
  • Exception types should help operations determine who needs to look at the error first.
  • Avoid using error codes to distinguish between unrelated types of errors that happen to be raised by the same method.
  • Never catch or throw ApplicationException.

 

Exceptions are an integral part of working with .NET, but far too many developers don’t think about them from an API design perspective. Most of their work begins and ends with knowing which exceptions they need to catch and which should be allowed to hit the global logger. You can significantly reduce the time it takes to correct bugs if you design the API to use exceptions correctly.

Whose fault is it?

The basic theory behind designing with exceptions begins with the question, “Whose fault is it?” For the purpose of this discussion, the answer will always be one of these three:

  • The Library
  • The Application
  • The Environment

When we say the “library” is at fault, we mean there is an internal flaw in whatever method is currently being executed. In this context, the “application” is the code that invoked the library’s method. (This is a bit of a fiction because both the library and the application code may be in the same assembly.) Finally there is the “environment”, which is anything external to the application that can’t be controlled.

Library Flaws

The quintessential library flaw is the NullReferenceException. There is never a legitimate reason for a library to throw a null reference exception that can observed by the application. If a null is encountered, the library code should always throw a more specific exception explaining what was null and how the problem should be corrected. For parameters this is clearly going to be an ArgumentNullException. If instead there is a null in a property or field, the InvalidOperationException is usually appropriate.

By definition, any exception that indicates a library flaw is a bug in the library that needs to be fixed. That doesn’t mean there isn’t also a bug in the application code, but the library needs to be fixed first. Only then is it appropriate to let the application developer know he has also made a mistake.

The reason for this rule is many people may be using the same library. If one person makes a mistake by passing in a null where it doesn’t belong, surely others will too. By replacing the NullReferenceException with one clearly indicating what went wrong, the application developers will immediately understand what went wrong.

The Pit of Success

If you read early literature on .NET design patterns, you’ll often come across the phrase “pit of success”. The basic concept is this: make the code easy to use correctly, hard to use incorrectly, and ensure the exceptions tell you what you did wrong. This philosophy of API design guides the developer into writing correct code almost by default.

This is why a naked NullReferenceException is so bad. Other than the stack trace, which may be quite deep into the library code, there is no information to help the developer figure out what they did wrong. ArgumentNullException and InvalidOperationException, on the other hand, give the library author a way to explain to the application developer how to fix the problem.

Other Library Flaws

The next library flaw is the ArithmeticException family. This includes DivideByZeroException, FiniteNumberException, and OverflowException. Again, this always represents an internal flaw in the library method, even if that flaw is just a missing parameter validation check.

Another example of a library flaw is the IndexOutOfRangeException. Semantically it is no different than an ArgumentOutOfRangeException, as seen in IList.Item, yet it only applies to array indexers. And since naked arrays are not normally used by application code, this implies that there is bug in a custom collection class.

ArrayTypeMismatchException has been rarely seen since .NET 2.0 introduced generic lists. The situation that triggers it is… well weird. From the documentation,

ArrayTypeMismatchException is thrown when the system cannot convert the element to the type declared for the array. For example, an element of type String cannot be stored in an Int32 array because conversion between these types is not supported. It is generally unnecessary for applications to throw this exception.

For this to happen, the aforementioned Int32 array has to be placed into a variable of type Object[]. If you are working with raw arrays, the library needs to check for this. For this reason, and many others, it is better to just not use raw arrays and instead wrap them in an appropriate collection class.

Other casting problems are usually revealed with the InvalidCastException. Continuing our same theme, type checks should mean InvalidCastException is never thrown and instead the caller gets an ArgumentException or an InvalidOperationException.

MemberAccessException is a base class covering a wide variety of reflection-based errors. In addition to the direct use of reflection, both COM interopt and incorrect use of the dynamic keyword can trigger it.

Application Flaws

The quintessential application flaw is the ArgumentException and its subclasses; ArgumentNullException, ArgumentOutOfRangeException. There are other subclasess you may not be aware of including:

All of these unequivocally indicate the application code is at fault and the flaw is on the line invoking the library method. Both parts of that statement are important. Consider this code:

foo.Customer = null; foo.Save();

If this were to throw an ArgumentNullException the application developer would be quite confused. Instead it should throw an InvalidOperationException to indicate something before the current line was messed up.

Exceptions as Documentation

A typical programmer doesn’t read the documentation. At least not at first. Instead he or she will read the public API, write some code, run it, and then, if it doesn’t work, search for the exception’s message on Stack Overflow. If the programmer is lucky, the answer will be readily found there with links to the right documentation. But even still, our programmer is unlikely to actually read it.

So as a library author how can we address this? The first step is to literally paste some of the documentation into the exception.

More Object State Exceptions

The most well-known subclass of InvalidOperationException is the ObjectDisposedException. Its use is pretty obvious, yet it isn’t unusual to see disposable classes that forget to throw this exception. The usual result of forgetting this is a NullReferenceException caused by the Dispose method nulling out disposable child objects.

Closely related to InvalidOperationException is the NotSupportedException. The difference between them is easy: InvalidOperationException means, “You can’t do that right now,” while “NotSupportedException” means, “You can never do that with this class”. In theory NotSupportedException should only occur when working with abstract interfaces.

For example, an immutable collection should throw a NotSupportedException in response to the IList.Add method. By contrast, a freezable collection would throw an InvalidOperationException when frozen.

An increasingly important subclass of NotSupportedException is PlatformNotSupportedException. This indicates the operation is allowed on some runtimes, but not others. For example, you may need to use this when porting code from .NET to UWP or .NET Core, as they don’t offer all the features found in the full .NET Framework.

The Problematic FormatException

Microsoft made a few mistakes when designing the first version of .NET. For example, FormatException is, logically speaking, a type of argument exception. The documentation even says “thrown when the format of an argument is invalid”. But for whatever reason, it doesn’t actually inherit from ArgumentException. Nor does it have a place to put the argument name.

Our tentative recommendation is to not throw FormatException. Instead, create your own subclass of ArgumentException called “ArgumentFormatException” or something to that effect. This will allow you to reduce debugging time by including essential information such as the argument name and the actual value being used.

This leads us back to our original thesis about “designing with exceptions”. Yes you could just throw a FormatException when your custom parser detects a problem, but that doesn’t help the application developer trying to use your library.

Environmental Flaws

An environmental flaw comes from the fact the world isn’t perfect. This includes scenarios such as when the database is down, a web server is unresponsive, a file is missing, etc. When environmental flaws appear in bug reports two things need to be considered:

  1. Did the application handle the flaw correctly?
  2. What in the environment caused the flaw?

Usually this is going to involve a division of labor. First off, the application developer is going to look into the answer for question number one. This doesn’t mean just error handling and recovery, it also means generating a useful log.

You may be wondering why we started with the application developer. The application developer has a responsibility to the operations team. If a call to a web server fails, the application developer can’t just throw up his arms and shout, “Not my problem”. He or she needs to first make sure the exception has enough detail to allow operations to do their job. If the exception just says “Server connection timeout”, how are they supposed to know which server was involved?

Specialized Exceptions

NotImplementedException

The NotImplementedException means one thing and one thing only: this feature is a work in progress. As such, the message for a NotImplementedException should always include a reference to your task tracking software. For example:

throw new NotImplementedException("See ticket #42.");

You could provide more details in the message, but realistically anything you write is going to be out of date almost immediately. So it is better to just direct the reader to the ticket where they can see things such as when you plan on implementing the feature.

AggregateException

The AggregateException is a necessary evil, but hard to work with. On its own it contains no information of value, all of the details are hidden in its InnerExceptions collection.

Since the AggregateException usually only contains one item, it seems logical for the library to unwrap it and return real exception. Normally you can’t rethrow an inner exception without destroying the original stack trace, but starting with .NET 4.5 there is a way to leverage ExceptionDispatchInfo.

Unwrapping an AggregateException

catch (AggregateException ex) { if (ex.InnerExceptions.Count == 1) //unwrap ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw(); else throw; //we actually need an AggregateException }

Unanswerable Cases

There are some exceptions that simply don’t fit into this scheme. For example, AccessViolationException indicates there was a problem reading unmanaged memory. Well, that could be caused by the native library code or could be caused by the application misusing the same. Only thorough research will reveal the nature of this bug.

Whenever possible, unanswerable exceptions should be avoided in your designs. In some cases, Visual Studio’s static code analyzer will even go so far as to flag violations of this guideline.

For example, ApplicationException is effectively deprecated. The Framework Design Guidelines explicitly says, “DO NOT throw or derive from ApplicationException.” And for good reason; an ApplicationException isn’t necessarily thrown by the application. Though that was the original intent, look at these subclasses:

  • Microsoft.JScript.BreakOutOfFinally
  • Microsoft.JScript.ContinueOutOfFinally
  • Microsoft.JScript.JScriptException
  • Microsoft.JScript.NoContextException
  • Microsoft.JScript.ReturnOutOfFinally
  • System.Reflection.InvalidFilterCriteriaException
  • System.Reflection.TargetException
  • System.Reflection.TargetInvocationException
  • System.Reflection.TargetParameterCountException
  • System.Threading.WaitHandleCannotBeOpenedException

Clearly some of these should be argument exceptions while others represent environmental problems. None of them are “application exceptions” because they are thrown only by libraries in the .NET framework.

Along the same lines, developers shouldn’t work directly with SystemException. Like ApplicationException, the subclasses of SystemException are all over the map, including ArgumentException, NullReferenceException, and AccessViolationException. Microsoft even goes so far as to suggest you forget this exists and only work with its subclasses.

A subcategoy of the unanswerable case are the infrastructure exceptions. We’ve already seen one, the AccessViolationException. Others include:

  • CannotUnloadAppDomainException
  • BadImageFormatException
  • DataMisalignedException
  • TypeLoadException
  • TypeUnloadedException

These tend the be very difficult to diagnose and may reveal esoteric bugs in either the library or the code that is calling it. So unlike ApplicationException, they legitimately fall into the unanswerable category.

Putting it into practice: Redesigning SqlException

Keeping these principles in mind, let’s take a look at SqlException. In addition to network errors where you can’t reach the server at all, there are over 11,000 distinct error codes in SQL Server’s master.dbo.sysmessages. So while the exception has all of the low level information you need, it is actually difficult to do anything with it beyond simply catch & log.

If we were to redesign SqlException, we would want it broken into distinct categories based on what we expect the user or developer to do.

SqlClient.NetworkException would represent all the error codes that indicate there is an environmental problem external to the database server itself.

SqlClient.InternalException would include the error codes that indicate something is critically wrong with the server such as database corruption or the inability to access a hard drive.

SqlClient.SyntaxException is our equivalent to ArgumentException. It means that you passed bad SQL to the server (either directly or by a bug in your ORM).

SqlClient.MissingObjectException would occur when the syntax is correct, but the database object (table, view, procedure, etc.) simply doesn’t exist.

SqlClient.DeadlockException happens when there is a conflict between two or more processes that try to modify the same information.

Each of these exception types imply a course of action:

  • SqlClient.NetworkException: Retry the operation. If it happens frequently, contact network ops.
  • SqlClient.InternalException: Contact a DBA immediately.
  • SqlClient.SyntaxException: Notify the application or database developer
  • SqlClient.MissingObjectException: Have ops check to see if something was missed during the last database deployment.
  • SqlClient.DeadlockException: Retry the operation. If it happens frequently, look for design errors.

To do this in real life we’d have to map all 11,000+ SQL Server error codes to one of those categories, a rather daunting proposition which explains why SqlException looks like it does.

Conclusions

When designing an API, organize your exceptions around the type of action that needs to be performed in order to correct the problem. This will make it easier to write self-correcting code, allows for more accurate logs, and makes routing the problem to the right person or team much faster.

About the Author

Jonathan Allen got his start working on MIS projects for a health clinic in the late 90's, bringing them up from Access and Excel to an enterprise solution by degrees. After spending five years writing automated trading systems for the financial sector, he became a consultant on a variety of projects including the UI for a robotic warehouse, the middle tier for cancer research software, and the big data needs of a major real estate insurance company. In his free time he enjoys studying and writing about martial arts from the 16th century.

Rate this Article

Adoption Stage
Style

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Tell us what you think

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread
Community comments

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Discuss

Login to InfoQ to interact with what matters most to you.


Recover your password...

Follow

Follow your favorite topics and editors

Quick overview of most important highlights in the industry and on the site.

Like

More signal, less noise

Build your own feed by choosing topics you want to read about and editors you want to hear from.

Notifications

Stay up-to-date

Set up your notifications and don't miss out on content that matters to you

BT