Error Handling in ASP.NET Razor Web Pages

Bang! An unhandled exception occurred during the execution of the current web request. And that's it - your visitor is met with the yellow screen of death and they are left wondering what they did wrong. You might not even be aware of what's just happened - unless your visitor can find some way of alerting you, oh, and they bother to do so. That's the problem with run time errors; the code worked fine when you ran it on your machine. But then along came a user and they tried to do something you didn't anticipate and broke your site. So what should you do about this?

The key to managing the issue is understanding it. The kind of errors I'm discussing in this article fall into two distinct categories: runtime errors that result from assumptions you made in code that didn't cover all the bases; and ones that result from the unforeseen failure of dependencies. Let's first look at assumptions made in code. Here's a simple page with a form and a bit of code to process the form's values when it is submitted. The code takes the numbers entered into each box and divides one by the other:

@{
    var result = string.Empty;
    if(IsPost){
        result = (Request["a"].AsDecimal() / Request["b"].AsDecimal()).ToString();
    }
}

<!DOCTYPE html>

<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>My Site's Title</title>
    </head>
    <body>
        <form method="post">
            <div>
                 @Html.TextBox("a") / @Html.TextBox("b") @result
            </div>      
            <div><input type="submit" /></div>
        </form>
    </body>
</html>

There's hardly any code here so what could go wrong? Well, if the user doesn't enter anything into the second textbox, a DivideByZeroException will be raised when the form is submitted. The same will happen if they enter text there (or, of course, zero). You can add some validation to ensure that the second text box contains a number greater than zero, and that's pretty easy with the Web Pages validation helpers. And that should take of that, shouldn't it? What if the user entered an HTML tag into one of the boxes? That will result in an HttpRequestValidationException

A potentially dangerous Request.Form value was detected from the client...

You validation helpers are of no use in this situation. They don't even get a chance to run before the exception is raised. Did you ever think that someone might enter HTML into a box intended for numbers? Probably not - but users have a million ways to surprise you.

Here's another code snippet:

@{
    var id = Request["id"].AsInt();
    var db = Database.Open("Books");
    var sql = "SELECT * FROM Books WHERE BookId = @0";
    var book = db.QuerySingle(sql, id);
}

This is a typical example of taking a value from the query string and passing it into a SQL command which gets executed against the database. What could go wrong here? If there is no matching id, the book variable will be null, so any attempt to reference its properties will result in a RuntimeBinderException

Cannot perform runtime binding on a null reference

You can mitigate against this by checking to see if the book is null before you start to work with it:

@if(book != null){
    <h1>@book.Title</h1>
}

However, what if someone clicks a link to your site that includes some extra stuff on the end like this:

http://www.yourdomain.com/book?id=1</strong>

The result will be another HttpRequestValidationException. You might wonder how your url would ever get into such a state. Well, it's actually all too easy for this kind of thing to happen as people paste your link into another place and the processing system mangles it up to include some HTML. You have no control over how your urls are treated by other people or systems, but if you ever look at the logs for any site, you might be surprised at the range of additions that get appended to requests - most of them innocent, but some of them designed to probe security weaknesses in your web application.

The other type of error results from the failure of an external component. SQL Server deadlocks will raise exceptions. A remote mail server might not be online or a DNS lookup might fail. Changes in permissions on directories on the web server resulting from an operating system update... All of these will result in a yellow screen of death.

So what should you do? You could wrap everything up in try-catch blocks. However, the standard advice is to only use try-catch if you can provide a meaningful route to recovery for the user. What you shouldn't do is

try{
    var id = Request["id"].AsInt();
}
catch(Exception){
        
}

There is nowhere for the code to go if the query string is tainted with HMTL. Not only that, but there is no record that anything went wrong.

Custom Error Page

Assuming that you aren't going to shield your entire code base with try-catch blocks, you are still left with the fact that ASP.NET error pages are not user friendly. What you should do is to provide your own custom error page instead. You should create a static HTML page for this. You don't want to use one of the ASP.NET page types tied to a layout or master page in case the error occurs somewhere in the layout page. Then your visitors will be presented with a different type of ASP.NET error page - one with a description that says

An exception occurred while processing your request. Additionally, another exception occurred while executing the custom error page for the first exception. The request has been terminated. 

Once you have created your error page, you need to tell your application to use it. You do this in your web.config file by adding a <customErrors> section to the <system.web> section. Here's the default web.config file from a WebMatrix site with the <customErrors> section added:

<?xml version="1.0" encoding="utf-8"?>

<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
    <customErrors mode="On" defaultRedirect="~/error.htm" />
  </system.web>
</configuration>

The mode property is set to On (with a capital O - a lower case o will result in a configuration error). If an unhandled exception arises in your code, the user will be redirected to your error.htm page. That may not be what you want when you are working on the application as you need to see the description of any errors that arise so you can fix them. But you don't want to have to remember to keep switching custom errors on and off. What you can do instead is to set the mode to RemoteOnly, which will result in the custom error pages being displayed to remote users, and detailed errors being displayed to requests made from the same machine that the web server is running on - i.e. your development box.

Exception Logging Modules And Handlers (ELMAH)

So now you have protected your users from the glare of a YSOD. However, when users contact you to say that they get an error page while browsing your site, how do you know what went wrong? Ideally, you need some way to log details of any exceptions that get raised. You can write a component to do that, or you can use one that's already been written, tested and tried out in thousands upon thousands of existing ASP.NET web sites. A great candidate is the open source ELMAH, which has enjoyed over 1.5M downloads via Nuget alone. If you are using the Nuget integration in WebMatrix, search for 'elmah' and install from there:

Error Handling

If you are using Visual Studio, invoke the Package Manager Console from Tools » Library Package Manager » Package Manager Console and at the PM> prompt, type

install-package elmah

Once the package is successfully installed, your web.config file look different and the handlers and modules that form the basis of Elmah are registered. Out of the box, Elmah logs to memory, which is ok for testing, but not for deployment where app restarts will result in the memory being cleared. Elmah will log to a variety of locations including email and databases - SQL Server full version and Compact are both supported, as are Sqlite and even Access. Here's a web.config which has been altered during installation, and then had a couple of lines added (highlighted) to show how to configure logging to a SQL Compact database:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <sectionGroup name="elmah">
      <section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah" />
      <section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" />
      <section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
      <section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah" />
    </sectionGroup>
  </configSections>
    <connectionStrings>
    <add name="Elmah" connectionString="data source=|DataDirectory|\Elmah.sdf" />
  </connectionStrings>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
    <customErrors mode="On" defaultRedirect="~/error.htm" />
    <httpModules>
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" />
      <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" />
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" />
    </httpModules>
  </system.web>
  <system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <modules>
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" preCondition="managedHandler" />
      <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" preCondition="managedHandler" />
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" preCondition="managedHandler" />
    </modules>
  </system.webServer>
  <elmah>
    <!--
        See http://code.google.com/p/elmah/wiki/SecuringErrorLogPages for 
        more information on remote access and securing ELMAH.
    -->
    <security allowRemoteAccess="false" />
    <errorLog type="Elmah.SqlServerCompactErrorLog, Elmah" connectionStringName="Elmah" />
  </elmah>
  <location path="elmah.axd" inheritInChildApplications="false">
    <system.web>
      <httpHandlers>
        <add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
      </httpHandlers>
      <!-- 
        See http://code.google.com/p/elmah/wiki/SecuringErrorLogPages for 
        more information on using ASP.NET authorization securing ELMAH.

      <authorization>
        <allow roles="admin" />
        <deny users="*" />  
      </authorization>
      -->
    </system.web>
    <system.webServer>
      <handlers>
        <add name="ELMAH" verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" preCondition="integratedMode" />
      </handlers>
    </system.webServer>
  </location>
</configuration>

If you want to log to a SQL Server database, the errorLog type property will be Elmah.SqlErrorLog instead:

<errorLog type="Elmah.SqlErrorLog, Elmah" connectionStringName="Elmah" />

If the database specified in the connection string does not exist Elmah will create one when Elmah logs its first error. Elmah will add one table - ELMAH_Error, to which it will log details of the exception it caught:

ASP.NET Error Handling

 

The AllXml field contains full details of any error in XML format, which includes not just the error message itself, but all of the Request.ServerVariables values to help with trouble shooting. This is especially useful for identifying mangled URLs that may appear somewhere else on the web so that you can take some remedial action.

Summary

The purpose of this article was to explain the importance of configuring a custom error page for your ASP.NET Razor Web Pages application to protect your users against unsightly default error pages. It also demonstrated that no matter how carefully you code, unforseen circumstances can lead to your application raising exceptions. The article also introduce Elmah for logging details of your errors and showed how easy it is to configure for use with a SQL Compact or SQL server database.