User-Facing Exceptions
After Mark Seeman’s recent blog post, Exception Messages are for Programmers, Mogens Heller Grabe commented:
I usually introduce a special exception when I’m building software for clients: a DomainException! As you might have guessed, it’s a special exception that is meant to be consumed by humanoids, i.e. the end user. This means that it is crafted to be displayed without a stack trace, providing all the necessary context through its Message alone. This is the exception that the domain model may throw from deep down, telling the user intimate details about a failed validation that could only be checked at that time and place.
I totally understand Mogens’s reasoning here. An exception’s primary goal is to halt execution in an error state, but there are some types of exceptions that really want to provide a user-facing message at the same time. They are still exceptional, but they know something about the nature of the error that would be useful for the user to be aware of.
However, there are some problems with Mogens’s strategy:
- Using a specific exception type (e.g.
DomainException
) means that you can’t use different exception types based on the nature of the exception, or other information that you might want to associate with it. What if I want aWorkflowDoesNotExistException
in one case, and aServiceNotAvailableException
in another, but I still want to provide a user-facing message with each? - As Mark Seeman very powerfully argues, exception messages are for programmers. Their purpose is to provide enough information for developers to know what went wrong, how to reproduce the issue, and how to fix it. The exception message is included with the stack trace when you call
ToString()
on the exception. Co-opting the exception message to provide user-facing information gets in the way of its primary purpose. - It’s good practice to catch exceptions and use them as the
InnerException
to a new exception with additional debug information, at various levels of the call stack. You can’t just catchDomainException
s and treat them differently from your other exceptions, because they could be thrown at any level of your application, and subsequently wrapped in other exceptions.
IUserFriendlyException
Here’s my solution to the problems mentioned above. I’ve been using it for a couple of years now without any problems.
public interface IUserFriendlyException
{
string UserFacingMessage { get; }
}
This solves the first problem by using an interface
, which can be implemented by any number of exception types.
It solves the second problem by using a new property–UserFacingMessage
–which can be provided in addition to (not instead of) your exception’s programmer-targeted Message
. Your programmers still get the debug information they want, and users aren’t exposed to technical details they don’t care about.
A Note Regarding Internationalization: While I haven’t personally had to worry about producing different user-facing messages for different users, Mark Seeman correctly points out that this is an important issue to consider in many applications. Fortunately, you can use this same pattern for projects that use the ResourceManager to produce culture-specific strings.
public interface IInternationalUserFriendlyException { string UserFacingMessageResourceKey { get; } }
But what about the third problem? This requires a little more work. It’s a good idea to catch and log all exceptions at the Presentation Layer of your program, so that users don’t get ugly server error pages, or crashing applications. You don’t care what type of exception it is, and you typically don’t have anything specific to say to the user at this point.
private static readonly string GenericErrorMessage =
"We are really sorry, but something unexpected happened. " +
"Please refresh the page and try again. " +
"If the problem persists, please report it to your administrator.";
public void OnException(ExceptionContext filterContext)
{
Log.Error(filterContext.Exception);
ShowError(filterContext, GenericErrorMessage);
filterContext.ExceptionHandled = true;
}
But now if you have a user-facing message, you can provide that message instead.
public void OnException(ExceptionContext filterContext)
{
Log.Error(filterContext.Exception);
ShowError(filterContext,
filterContext.Exception
.AndInnerExceptions()
.OfType<IUserFriendlyException>()
.Select(e => e.UserFacingMessage)
.FirstOrDefault()
?? GenericErrorMessage);
filterContext.ExceptionHandled = true;
}
The only tricky part is that you have to dig deeper than the top-level message. The code above uses this extension method:
public static IEnumerable<Exception> AndInnerExceptions(
this Exception exception)
{
while (exception != null)
{
yield return exception;
exception = exception.InnerException;
}
}
Conclusion
I’ve found this strategy to work well in those cases where an exception wants to communicate user-facing information. Hopefully you will, too. Do you have questions or comments? Leave them in the comments section below.