static void

ASP Errors (handling exceptions)

NB: not data validation! For WebAPI it's a little different- see below

Raising errors

You can return "RESTful" HTTP status codes:

//404
if (category > 0)
    return HttpNotFound("No such category"); //new HttpNotFoundResult()
 
//400
if (category == -1)
    return new HttpStatusCodeResult(System.Net.HttpStatusCode.BadRequest,
        "Logical error.");
 
//401, when AuthorizeAttribute is not enough
if (User.Identity.IsAuthenticated && !User.IsInRole("Admin"))
    return new HttpUnauthorizedResult("Unauthorized");
 
//500
if (category == -2)
    throw new HttpException("Generic error");
if (category == -3)
    throw new InvalidOperationException("Generic error"); //also gets wrapped in HttpException

Catching errors

The stack (from closest to exception upwards)

HandleError

By default, App_Start/FilterConfig will add a global [HandleError] attribute.

You can also add other HandleError attributes on controllers or actions, for specific errors and using specific views (set Order to control precedence - high order = high priority).

[HandleError(ExceptionType = typeof(InvalidOperationException))]

HandleError detects normal exceptions (not 404s) and redirects to a view Error.cshtml, either in the same controller or /Shared.

NB: system.web/customErrors[mode="On"] must be set, otherwise it ignores exceptions (and shows the YSOD, which in debug is what you want)

The default templated Error.cshtml is full html, but you can use _Layout and use a HandleErrorInfo model.

@model System.Web.Mvc.HandleErrorInfo
 
@{
    ViewBag.Title = "Test";
}
 
<h1>Error Handled</h1>
<p>Controller=@Model.ControllerName action=@Model.ActionName exception=@Model.Exception.Message</p>

A view with no controller is awkward. You can't log errors (you shouldn't do logic in the view). You may prefer to not use a global HandleError and use web.config customErrors or global.asax Application_Error instead.

You can add a new logging filter in App_Start/FilterConfig before the HandleError

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new ErrorLoggerFilter());
    filters.Add(new HandleErrorAttribute());
}

The Error logging filter looks like this. Substitute the logging framework of your choice.

public class ErrorLoggerFilter : IExceptionFilter
{
    public void OnException(ExceptionContext filterContext)
    {
        System.Diagnostics.Trace.TraceError(
            "LogFilter: " + filterContext.Exception);
 
        //if using ELMAH
        if (filterContext.ExceptionHandled)
            Elmah.ErrorSignal.FromCurrentContext().Raise(filterContext.Exception);
    }
}

Routing 404s

"Catch-all" routes (with asterisk prefix) don't work well. This has to be after the default "controller/action/id" route, but any url of the form x/y or /y/z never hits the catch all (you will trap 1/2/3/4)

routes.MapRoute(
    "404",
    "{*url}",
    new { controller = "Error", action = "NotFound" }
);

As this is basically useless, it's better to just use customErrors, below

web.config customErrors

The traditional ASP config: system.web/customErrors[mode="On"]. By default this does a 302.

<system.web>
  <customErrors mode="On" defaultRedirect="/Error/">
    <error statusCode="404" redirect="/Error/NotFound" />
  </customErrors>

redirectMode="ResponseRewrite" cannot redirect to a route, only to a physical html or aspx file.

[HandleError] intercepts most errors before this (but not 404s). Unlike [HandleError] you redirect to a controller, but the exception isn't passed on so you can't log it (use a logging exception filter or global.asax Application_Error).

httpErrors

This is only used by IIS7. customErrors will trap errors first, and httpErrors catches higher level IIS level exceptions like 404s on .html files.

<system.webServer>
  <httpErrors errorMode="Custom">
    <!-- catch all non-ASP 404s (.jpg, .html) -->
    <remove statusCode="404" />
    <error statusCode="404" path="/Error/NotFound" />
  </httpErrors>

customErrors mode should still be set to On

Error controller

You simply set the HTTP status code and Response.TrySkipIisCustomErrors to true. But you don't have access to the original exception (you arrive at the controller from a 302 redirect).

public class ErrorController : Controller
{
    // GET: /Error/
    public ActionResult Index()
    {
        Response.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError; //500
        Response.TrySkipIisCustomErrors = true;
        return View();
    }
 
    // GET: /Error/NotFound
    public ActionResult NotFound()
    {
        Response.StatusCode = (int)System.Net.HttpStatusCode.NotFound; //404
        Response.TrySkipIisCustomErrors = true;
        return View();
    }
}

Global.asax Application_Error

The "classic" asp.net solution is still preferable to HandleError. It fires before customErrors. If you don't call Server.ClearError the customErrors takes over. It is not fired if there is a [HandleError] attribute. If you are not logging exceptions through an action filter, you can reliably log here.

protected void Application_Error()
{
    var exception = Server.GetLastError();
    Response.Clear();
 
    //customErrors=On/RemoteOnly+nonLocal
    if (!HttpContext.Current.IsCustomErrorEnabled)
        return; //wants to see YSOD
 
    //log it if necessary
    System.Diagnostics.Trace.TraceError(exception.ToString());
 
    //is this a specific error?
    var httpException = exception as HttpException;
    string action = null;
    if (httpException != null)
    {
        var code = httpException.GetHttpCode();
        if (code == 404)
            action = "NotFound";
    }
    Server.ClearError(); //make sure customErrors doesn't take over
    Response.TrySkipIisCustomErrors = true; //don't let IIS7 take over
    Response.Redirect(String.Format("~/Error/{0}", action));
}

WebAPI

global.asax Application_Error is NOT fired, but it does obey customErrors mode for whether an exception is returned in Json/xml.

www.asp.net overview

From 2014, webapi has ErrorLogger and ExceptionHandler base classes which can be inherited and applied for true global logging and handling.

if (id == 1)
    throw new HttpResponseException(HttpStatusCode.NotFound);
if (id == 2)
{
    var responce = new HttpResponseMessage(HttpStatusCode.NotFound);
    responce.ReasonPhrase = "Item not found"; //HTTP level
    responce.Content = new StringContent("No item with id 2"); //message
    throw new HttpResponseException(responce);
}

HttpResponseMessage methods

if (id == 0) //createResponse
    return Request.CreateResponse(HttpStatusCode.NotFound, "Cannot find id");
if (id == 1) //using HttpError
    return Request.CreateResponse(HttpStatusCode.BadRequest, new HttpError("Bad input"));
if (id == 2) //createErrorResponse
    return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Bad input");
if (!ModelState.IsValid) //modelstate
    return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);

The standard json error response is { Message: x }

WebAPI filters

[HandleError] doesn't work on webapi!

You can use a limited equivalent by implementing the abstract ExceptionFilterAttribute. It is limited as it only traps raw .net exceptions, and ignores HttpResponseExceptions (which prior to WebApi 2.1 can't be logged...)

public class HandleExceptionAttribute : System.Web.Http.Filters.ExceptionFilterAttribute
{
    //MVC's [HandleError] doesn't work (it returns a view)
 
    public override void OnException(System.Web.Http.Filters.HttpActionExecutedContext actionExecutedContext)
    {
        //log it
        Trace.TraceError(actionExecutedContext.Exception.ToString());
        //handle it
        actionExecutedContext.Response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
    }
}

Then apply the attribute on action or controller:

[HandleExceptionAttribute]
public int Post(Order order)

Or into GlobalConfiguration filters in App_Start (NB: not MVC action filters!!)

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services
        config.Filters.Add(new HandleExceptionAttribute());

WebAPI 2.1 Loggers and Handlers

Global logging and handling is only available from WebApi 2.1 (January 2014+)

// There can be multiple exception loggers (by default there are 0)
config.Services.Add(typeof(System.Web.Http.ExceptionHandling.IExceptionLogger),
    new MyExceptionLogger());
 
// There must be exactly 1 exception handler. (There is a default one that may be replaced.)
config.Services.Replace(typeof(System.Web.Http.ExceptionHandling.IExceptionHandler),
    new MyExceptionHandler());

Implementations:

public class MyExceptionLogger : System.Web.Http.ExceptionHandling.ExceptionLogger
{
    public override void Log(System.Web.Http.ExceptionHandling.ExceptionLoggerContext context)
    {
        Trace.TraceError("Method {0} url {1} exception {2}",
            context.Request.Method,
            context.Request.RequestUri,
            context.Exception);
    }
}
 
public class MyExceptionHandler : System.Web.Http.ExceptionHandling.ExceptionHandler
{
    public override void Handle(System.Web.Http.ExceptionHandling.ExceptionHandlerContext context)
    {
        //return an IHttpActionResult
        context.Result = new InternalServerErrorResult(context.Request);
    }
}

Async Disconnect Errors (.net 4.5)

In .net 4.5 you can use async Task actions. Because they are async, disconnects trigger the standard escalation policy to terminate the process. Opps. Put this in Global.asax Application_Start:

//log and swallow the async disconnect errors "The remote host closed the connection. The error code is 0x80070057."
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
    e.SetObserved();
    Trace.TraceError(e.Exception.ToString());
};