Wrapping exceptions for fun and profit

One for the programmers. (If you’re not one, look away now.)

Whenever I work on a nontrivial C# program, I usually end up writing the extension method shown at the bottom of this post. It lets me provide informative error messages when an exception bubbles up to the user, and promotes good error-handling generally. This is me sharing it with you, so you can enjoy it too.

Given an Exception, possibly containing inner exceptions, ‘ToFriendlyString()’ produces an ‘explanation’ from the chain of Exception.Messages: “This failed, because that failed, because the other failed.” For example:

Unable to create new order
because: Cannot get tax rate
because: Could not read settings file.
because: Access to the path "c:\someprogram\settings.ini" is denied.

It provides more information than you’d get from just printing Exception.Message, but less ridiculously overwhelming than showing the user the stack-trace.

Motivation

The problem with a lot of user-level error messages is that they are too general (“Something has gone wrong while saving Customer record!”) or uselessly precise (“Network socket closed unexpectedly”). Often it is useful to provide high-level and low-level detail of what went wrong, to help the user understand and possibly resolve the fault.

These multiple levels of detail can be provided by nesting (or ‘chaining’ in Java) exceptions, with higher-level exceptions wrapping lower-level ones.

Clearly, many exceptions are handled internally and never seen by the user. That’s as it should be. But when we do need to report an error, we should report it clearly.

Similarly, many applications write errors to a log. Programming faults are the only exceptions which really need their entire stack trace logged. Logic or environmental faults don’t indicate a bug to be fixed, and if they need to be logged, the ‘explanation’ is the only thing which usefully needs to be recorded.

It improves how you structure exceptions

By adopting this mechanism, you’re encouraged to align the structure of your exception hierarchy with the logical, semantic layers of your application.

Best Practices for Exception Handling at O’Reilly’s onjava.com gives a good primer on designing your exception hierarchy. One key observation is that there are 3 different types of fault:

  1. Programming faults—e.g., IndexOutOfRangeException
  2. Logic faults—e.g., the caller tried to perform a disallowed action
  3. External or environmental faults—e.g., we ran out of memory, or the network transfer failed.

Type-2 exceptions at one level may become type-1 exceptions at a higher level.

At each layer, you should catch-and-wrap exceptions from lower layers, or from other subsystems, and wrap them in exceptions which express what the higher layer was trying to do. For example, if you’re performing an operation which needs to write to a temporary file, catch any IOExceptions thrown by the file-processing logic, and wrap them in a higher-level ‘writing temporary file’ exception so that the user sees something like “Cannot convert to PDF, because: Cannot write to temporary file, because: Out of disk space”. You should wrap exceptions where it provides useful information to the user, or groups a class of error conditions which other subsystems may want to handle.

When you wrap an exception, you should not just repeat the message from the inner exception. The message of your new exception should reflect the higher semantic level of the wrapper.

Exceptions caused by programming flaws need not necessarily be caught and wrapped. But you should consider how they’re presented. More of this below.

Designing your exception hierarchy

Adopting this approach to showing errors imposes an additional design constraint on your software, because you’re now designing an exception hierarchy which:

  • (as before) enables client code to catch the exceptions in which it is interested;
  • (and also) supports succinct and accurate explanations of faults.

I contend that these two constraints complement each other, and together encourage good overall design.

Your inheritance structure of exceptions should reflect the classes of error they represent, and how they are likely to be caught, (not necessarily which objects throw them). Exception base classes should represent good categorisations of faults.

Do not shoehorn the use of an exception, or ‘pun’ by throwing an exception which sounds like it could be appropriate, but actually represents a different kind of fault. Instead, create a new class which represents your condition. Conversely, if two fault conditions are the same, they should throw the same exception class (or at least throw exceptions which inherit from the same base class).

As a corollary, each of your exception classes must clearly define which class of faults it represents, when it is appropriate, and when it would be inappropriate.

For example, IOException is a good example of an exception base class, because there are many cases in which you want to distinguish the subclasses of IOException from other broad categories of error conditions. .NET’s SystemException is a particularly useless exception class because it cuts across many different fault types.

You will want to avoid showing the user exception messages which indicate programming faults (like array-index-out-of-bounds, or null reference exceptions), so, at the very least, your exception hierarchy should make it easy to distinguish programming fault exceptions from business logic and external-condition exceptions.

Variants on the theme

If you’re really paying attention to your error messages and how they’re presented to the user (and of course you should), you will also want to customise the method to control which exceptions are shown. The full exception stack-trace of errors should be available in your application’s log file, so it’s fine to cut elements out of the explanation shown to the user if they do not add clarity.

Create your own variant of ‘ToFriendlyString()’ which rewords some exception messages and omits some classes.

One trivial improvement would be to avoid showing repeated messages in the exception chain.

You should suppress any exceptions indicating a programming fault—and replace with a statement merely that there was a fault— “Internal error” would be a cop-out message; “A fault in the software” would be more honest. Details of how your software bugs are expressed is not useful information for your users, and displaying them could potentially expose security weaknesses to attackers.

Additionally, certain specific exception classes may be meaningless, or provide no additional information to the user. This will depend upon the specific frameworks and APIs you’re using. For example, Velocity.Runtime.Parser.ParseExceptions are always wrapped in another exception which repeats the error message, so you could stop recursion there. Clearly you’ll need to deal with this on a case-by-case basis.

Better error messages & localisation

There is much, much more that could be said about this topic. For starters:

Error messages presented to the user should be in the user’s own language, but exceptions logged as a stack trace, or emailed to the developer, should be in a language which the user can understand. Java provides the Exception.getLocalizedMessage() method; unfortunately the .NET framework does not have support for localising exception messages.

To support localisation, your Exception objects should be record classes which embody structure as well as a message string. For example, your “File not found” exception needs to store the file name, so that the message could be localised as, “File ‘{0}’ not found”.

Even if an error message is localised, it should be possible for the developer easily to identify the error in their own language. So errors could also be assigned unique codes which are displayed. A code may also easier for a user to report than a detailed message too.

Error messages should be easily Google-able. Again, unique codes help with this.

Summing up

Good exception handling is important to stable, predictable software. Using exception handling to drive error reporting too, takes advantage of existing exception design effort, and encourages exceptions to be designed thoroughly and well.

The code

This is just the basic version (in C#), without any special cases or localisation. Enjoy:


public static class ExceptionExtensions
{
	public static string ToFriendlyString(this Exception exception, string separator = "\nBecause: ")
	{
		var result = new StringBuilder(exception.Message);
		var inner = exception.InnerException;
		while (inner != null)
		{
			result.Append(separator);
			result.Append(inner.Message);
			inner = inner.InnerException;
		}

		return result.ToString();
	}
}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.