I'm sure I don't need to tell you how bad serving a Yellow Screen of Death to your users is. Nonetheless, it seems to be pretty common practice across the web. One of the first things I do when setting up a new ASP.NET project is set up custom error pages and ensure all exceptions are logged (who wants to find out about their errors from their visitors?). Since things work a little differently in ASP.NET MVC, I thought I'd dig in and find the best way to do the same sort of thing.

The HandleError Attribute

The HandleError attribute (which appears on the default controllers in an MVC project) tells the framework that if an unhandled exception occurs in your controller that rather than showing the default Yellow Screen of Death it should instead serve up a view called Error. The controller-specific View folder will be checked first (eg. Views/Home/Error.aspx) and if it's not found, the Shared folder (Views/Shared/Error.aspx) will be used.

But How Do I Log Exceptions?

You might've spotted the problem with HandleError. It just outputs a view, and doesn't let you run any code. This might be fine if you don't want users to see errors but don't really care for fixing them. Hopefully you think this isn't acceptable and you want to investigate all exceptions!

The OnException Method

The System.Web.Mvc.Controller class contains a method called OnException which is called whenever an exception occuts within an action. This does not rely on the HandleError attribute being set. If you're being a good coder and have your own base Controller class you can override this method in one place to handle/log all errors for your site. You might choose to send emails and/or detect duplicate exceptions and discard them. For now, I'm just going to write them all to a text file in my App_Data folder.

protected override void OnException(ExceptionContext filterContext)
{
	WriteLog(Settings.LogErrorFile, filterContext.Exception.ToString());
}

/// <summary>
/// Logs a message to the given log file
/// </summary>
/// <param name="logFile">The filename to log to</param>
/// <param name="text">The message to log</param>
static void WriteLog(string logFile, string text)
{
	//TODO: Format nicer
	StringBuilder message = new StringBuilder();
	message.AppendLine(DateTime.Now.ToString());
	message.AppendLine(text);
	message.AppendLine("=========================================");

	System.IO.File.AppendAllText(logFile, message.ToString());
}

This works great, but it still shows our user an unhandled exception message, even if we use the HandleError attribute. This makes the HandleError attribute look rather useless, so I've removed it. We can easily show the friendly error ourselves with the following code:

filterContext.ExceptionHandled = true;
this.View("Error").ExecuteResult(this.ControllerContext);

It's important to set ExceptionHandled to true, otherwise you'll still see the default unhandled exception message. The OnException method returns void so we must Execute the view and pass in the ControllerContext ourselves.

How Do I see my own Errors During Development?

It's a little inconvenient to open log files or keep commenting out your error handling code while developing to see exceptions and stack traces. You might remember ASP.NET has a nice web.config setting that configures custom errors. This property is exposed via MVC, so we can set up our config to show friendly errors to remote users only:

<customErrors mode="RemoteOnly" />

Then all we need to do in our OnException method is check this value and serve up the custom error view only if it returns true.

protected override void OnException(ExceptionContext filterContext)
{
	WriteLog(Settings.LogErrorFile, filterContext.Exception.ToString());

	// Output a nice error page
	if (filterContext.HttpContext.IsCustomErrorEnabled)
	{
		filterContext.ExceptionHandled = true;
		this.View("Error").ExecuteResult(this.ControllerContext);
	}
}

It's worth noting that IsCustomErrorEnabled will resolve the RemoteOnly option for you, you don't need to check where the user is coming from. Now out site serves up friendly errors to users and logs all exceptions without us losing the ability to see stack traces during development.